理解javascript函数调用和this

Understanding JavaScript Function Invocation and 'this'

Posted by wuqiuyu on November 13, 2017

原文地址Understanding JavaScript Function Invocation and ‘this’

  多年来,我碰到很多关于Javascript的函数调用的困惑。
尤其是很多人抱怨在调用javascript函数的时候,会对this的意思产生困惑。
  在我看来,所以的这些困惑,在理解了原生的核心函数调用,
将所有的其他函数调用都看成是这些元素函数的语法糖。事实上,ECMAScript规范就是这样认为的。
在某些方面,这篇文章是对规范的简化,但是基本思想是一样的。

原生的核心

首先,让我们来看一看原生核心函数,call方法[1]。call方法是相对直接的。
  1、在第一个参数之外,传入一个参数列表(arglist)。
  2、第一个参数传入thisValue。
  3、将函数的this指向这个thisValue,函数的参数列表是arglist。
例如:

function hello(thing) {
  console.log(this + " says hello " + thing);
}

hello.call("Yehuda", "world") //=> Yehuda says hello world

  正如你所看到的,我们将hello方法的this指向“Yehuda”,并且传入一个参数“world”。
。这就是原生javascript核心函数调用。可以认为其他的所有的calls函数,都是原生语法糖。
(语法糖是指用一个更方便语法和一个更基本的核心原生术语描述它)

[1]在es5规范中,call方法用另外一种方法描述,更加的低级的原生。但是真是一个非常轻的包装,
所以我在这里简化了一点。想了解更多信息请看文章末尾。

简单的函数调用

  显然的,每次都用call方法调用函数有点烦人。Javascript允许我们直接使用括号调用函数(hello(“world”))。
当我们这样使用的时候,方法是这样的:

 function hello(thing) {
  console.log("Hello " + thing);
}

// this:
hello("world")

// desugars to:
hello.call(window, "world");

  在ECMAScript 5下,在使用严格模式[2]的时候表现会不一样:

// this:
hello("world")

// desugars to:
hello.call(undefined, "world");

  简单的讲就是:函数用fn(…args)和fn.call(window[ES5-strict: undefined], …args)方式调用是一样的。
  但是要注意,(function() {})()和 (function() {}).call(window [ES5-strict: undefined)这两种匿名函数的调用方式也是一样的。

[2]事实上,我撒了一点小谎。ECMAScript 5规范规定undefined总是会被滤过,但是在非严格模式下,在函数调用的时候应该把this值指向全局变量。这个也做可以避免在严格模式下使用那些非严格模式的库的时候不出错。

成员函数

  另外一种非常普遍的函数调用方法是作为一个对象的成员(person.hello())。这种情况下,调用方法是这样的:

var person = {
  name: "Brendan Eich",
  hello: function(thing) {
    console.log(this + " says hello " + thing);
  }
}

// this:
person.hello("world")

// desugars to this:
person.hello.call(person, "world");

  要知道,在这种情况下hello方法和对象的是如何绑定的并不重要。还记得我们之前定义了一个独立的hello函数,
让我们来看看动态的把hello函数绑定到对象上会发生什么:

function hello(thing) {
  console.log(this + " says hello " + thing);
}

person = { name: "Brendan Eich" }
person.hello = hello;

person.hello("world") // still desugars to person.hello.call(person, "world")

hello("world") // "[object DOMWindow]world"

  函数中没有永久的this这个概念。通常在调用的时候根据调用的方法确定。

使用Function.prototype.bind

  有史以来人们一直习惯使用闭包来防止this漂移,因为在某些情况下这个方法可以很简单的给函数一个永久的this值。

var person = {
  name: "Brendan Eich",
  hello: function(thing) {
    console.log(this.name + " says hello " + thing);
  }
}

var boundHello = function(thing) { return person.hello.call(person, thing); }

boundHello("world");

  即使我们使用boundHello.call(window, “world”)的方法调用boundHello,我们也无法将this值指向我们想要的。 
我们可以做一些调整,使这个小技巧更加的通用:

var bind = function(func, thisValue) {
  return function() {
    return func.apply(thisValue, arguments);
  }
}

var boundHello = bind(person.hello, person);
boundHello("world") // "Brendan Eich says hello world"

  为了理解这个,你只需要知道两点。首先,arguments是类数组对象,它代表了传递给函数的参数。
其次,apply方法和call方法类似,除了它接受的参数是一组类数组对象之外。
  我们的bind方法只是返回了一个新的函数。当函数被调用的时候,新的函数只是将原始函数的this值指向传
入的this值,并且调用了原始函数。同样都通过参数传递。 因为这个方法经常被使用,所以ES5给所以的函数对象新添加了一个bind方法,像下面这样:

var boundHello = person.hello.bind(person);
boundHello("world") // "Brendan Eich says hello world"

  当我们希望把一个原函数作为回调函数传入时,这种方法尤为实用用:

var person = {
  name: "Alex Russell",
  hello: function() { console.log(this.name + " says hello world"); }
}

$("#some-div").click(person.hello.bind(person));

// when the div is clicked, "Alex Russell says hello world" is printed

  当然这种方法是笨重的,并且TC39(正在编写下一代ECMAScript的组织)正在努力创造一种更加优雅和
向后兼容的解决方法。

关于jQuery

  因为jQuery使用了很多匿名回调函数,它在内部使用call方法将这些回调函数的this值设置为更加有用的
值。例如,在所有的事件处理函数中,jQuery在element调用回调函数的时候传入的this值是element
自身,而不是window(在你没有特殊干预的情况下)。 这非常的实用,因为这不仅使得默认的this值在匿名回调函数中不是特别的有用,而且也会给新学习javascript
的人留下印象就是,this值很难理解。   如果你理解了函数基本的调用的语法糖func.call(thisValue, …args),
你应该能够理解javascript变幻莫测的this值。

PS:我撒了谎

  在很多地方,我都简化的规范里的说法。最明显的地方就是我将func.call这种函数调用方法当作一个原生
函数调用方法。事实上,规范里,func.call和[obj.]func()都实用。
然而让我们来看看func.call的定义:
    1、如果不能被调用,则抛出一个类型错误。
    2、参数列表为空。
    3、如果函数传入了不止一个参数,则从左到右依次将参数作为最后一个参数加入到参数列表中。
    4、返回内部函数的调用结果,将传入的this止值作为this,传入的参数作为参数列表
如你所见,这个定义就是非常简单的javascript语法。