原文链接:原文链接
多年来,我已经看到许多关于 JavaScript 函数调用的困惑。特别是,许多人抱怨函数调用中的 this
语义令人困惑。
在我看来,通过理解核心函数调用语句,然后在该原语之上查看以糖为例调用功能的所有其他方式,可以消除许多此类混淆。
核心原函数
首先,让我们看一下核心函数调用原语,call
函数的调用方法。调用方法相对简单。
- 从参数 1 到结尾创建参数列表(
argList
) - 第一个参数是
thisValue
- 将此函数设置为此 thisValue 并将 argList 作为其参数列表来调用该函数.
例如:
function hello(thing){console.log(this + "says hello" + thing)
}
hello.call("Mandy", "world") // Mandy says hello world
我们调用 hello
方法,他
它的 this
被设置成 Mandy
,还有一个参数world
。 这是 JavaScript 函数调用的核心原语。您可以将所有其他函数调用视为对该核心原语的替代(“替代”是采用一种方便的语法,并以更基本的核心原语进行描述)。
简单函数调用
显然,一直使用 call
调用函数会很烦人。JavaScript
使我们可以使用语法 (hello("world"))
直接调用函数。当我们这样做时,调用将会被替代。
function hello(thing){console.log("Hello" + thing)
}
// this:
hello("world")
// desugars to:
hello.call(window, "world");
当使用严格模式 (use strict) 时,相当于
// 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)
。
成员方法
方法调用中另一种个非常常见的调用方式是:方法作为对象的成员调用(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")
// [object Object] says hello world
hello("world") // "[object DOMWindow]world"
// [object Window] says hello 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"); // Brendan Eich says hello world
即使我们的 boundHello("world")
调用仍然相当于 boundHello.call(window,"world")
,我们还是转过来使用我们的原始call
方法将 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
是一个类似 Array 的对象,表示传递给函数的所有参数。其次,apply
方法的工作方式与 call
完全相同,不同之处在于它采用了一个类似于 Array 的对象,而不是一次列出一个参数。
我们的 bind
方法返回一个新函数。调用它时,我们的新函数将简单地调用传入的原始函数,并将原始值设置为this
。它还传递参数。
因为这是一个有点普遍的习惯用法,所以 ES5 在所有实现此行为的 Function
对象上引入了一种新的 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 下一版本的委员会)继续致力于开发一种更加优雅,仍然向后兼容的解决方案。