JavaScript 的函数调用与 this

JavaScript 函数有多种调用方式,每种方式的不同在于函数内 this 的初始化。对 this 关键字有解释:一般而言,在 JavaScript 中,this 指向函数执行时的当前对象。注意 this 是保留关键字,你不能修改 this 的值。

作为一个函数调用

先看一个最简单的实例,在浏览器中:

1
2
3
4
function myFunction() {
return this;
}
alert(myFunction()); // [object Window]

而当函数没有被自身的对象调用时,this 的值就会变成全局对象。在 Web 浏览器中全局对象是浏览器窗口(window 对象)。该实例返回 this 的值是 window 对象。也就是此函数即为 window 对象的函数,myFunction() 等同于 window.myFunction()。但是此处在使用内部函数时存在一个 this 指向的问题,看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 注意此处在Node环境和浏览器环境下值不同,
// Node环境下必须去掉var使value成为全局变量。
value = 4;

var myObject = {
value: 1,
double: function() {
//var that = this;
var helper = function() {
this.value = add(this.value, this.value);
};
helper();
}
};

function add(a, b) {
return a + b;
}

myObject.double();
alert(value); // 8
alert(myObject.value); // 1

在这里的 myObject 对象中,我们在 double 这个函数中使用了内部函数并赋值给 helper但是此处的 this.valuethis 指向了全局对象,所以在执行这个函数后全局变量 value 的值变了但 Object 中的 value 属性值仍然是 1,这不是我们想要的结果。《JavaScript 语言精粹》中指出这里是语言设计上的一个错误,this 应该仍然绑定到外部函数的 this 变量中。这个设计错误的后果就是方法不能利用内部函数来帮助它工作,因为内部函数的 this 被绑定了错误的值,所以不能共享该方法对对象的访问权。但是我们可以有一个很容易的解决方案去解决这个问题,对 myObject 进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var myObject = {
value: 1,
double: function() {
var that = this;
var helper = function() {
that.value = add(that.value, that.value);
};
helper();
}
};

myObject.double();
alert(value); // 4
alert(myObject.value); // 2

这里使用了一个 that 变量来指向 double 方法中 this 的值即 myObject 本身,这样就可以对 myObject 对象的属性 value 进行修改。

函数作为方法调用

这种方式即为,一个函数被保存为一个对象的属性,在此时此函数被成为一个方法。调用时 this 关键字被绑定到该对象,即:函数作为对象方法调用,会使得 this 的值成为对象本身。实例如下:

1
2
3
4
5
6
7
8
var myObject = {
firstName: "John",
lastName: "Doe",
fullName: function() {
return this.firstName + " " + this.lastName;
}
}
myObject.fullName(); //返回 "John Doe"

另一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
var myObject = {
value: 0,
increment: function (inc) {
this.value += typeof inc === 'number' ? inc : 1;
}
};

myObject.increment();
console.log(myObject.value); // 1

myObject.increment(2);
console.log(myObject.value); // 3

在此例中,increment 方法对 myObject 对象中的 value 属性进行增加操作,increment 函数中的 this 即指向 myObject 对象本身。

构造器调用模式

如果函数调用前使用了 new 关键字,则是调用了构造函数。这看起来就像创建了新的函数,但实际上 JavaScript 函数是重新创建的对象:

1
2
3
4
5
6
7
8
// 构造函数
function myFunction(arg1, arg2) {
this.firstName = arg1;
this.lastName = arg2;
}
// This creates a new object
var x = new myFunction("John", "Doe");
x.firstName; //返回 "John"

构造函数的调用会创建一个新的对象。新对象会继承构造函数的属性和方法。
构造函数中 this 关键字没有任何的值。this 的值在函数调用时实例化对象 (new object) 时创建。

看另一个例子:

1
2
3
4
5
6
7
8
9
10
var Quo = function (string) {
this.status = string;
};

Quo.prototype.get_status = function() {
return this.status;
};

var myQuo = new Quo("confused");
console.log(myQuo.get_status()); // confused

输出结果为 confused。在这里使用 new 来调用时,会创建一个连接到该函数的 prototype 成员的新对象,同时 this 会被绑定到这个新对象上。即此时我们创建了 myQuo 对象,其连接到了 Quoprototype,且创建时的 this.status = string; 中的 this 指向这个创建的新对象,status 为新对象 myQuo 的属性,而不是 Quo 函数 prototype 的属性,可以做如下验证:

1
console.log(myQuo.hasOwnProperty("status")); //true

作为对比,我们对 Quo 函数增加一个属性 id:

1
2
3
4
var Quo = function (string) {
this.status = string;
};
Quo.prototype.id = 1;

idQuo 构造器函数 prototype 对象的一个属性,然后再次进行测试:

1
2
3
4
var myQuo = new Quo("confused");
console.log(myQuo.id); //1
console.log(myQuo.hasOwnProperty("get_status")); //false
console.log(myQuo.hasOwnProperty("id")); //false

这里可以看到在创建新对象 myQuo 时没有 id 属性和 get_status 函数,而这里 myQuo 继承了 Quoid 属性,就通过原型链找到了 Quo.prototype.id 的值。

apply 调用模式

在 JavaScript 中,函数是对象。JavaScript 函数有它的属性和方法,call()apply() 就是预定义的函数方法。这两个方法可用于调用函数。
对于 apply

  • 在 JavaScript 严格模式 (strict mode) 下,在调用函数时第一个参数会成为 this 的值,即使该参数不是一个对象。
  • 在 JavaScript 非严格模式 (non-strict mode) 下,如果第一个参数的值是 nullundefined,它将使用全局对象替代。
1
2
3
4
5
6
7
function myFunction(a, b) {
return a * b;
}
myFunction.call(myFunction, 10, 2); //返回20

myArray = [10,3];
myFunction.apply(null, myArray); //返回30

可见两者的区别在于第二个参数:apply 传入的是一个参数数组,也就是将多个参数组合成为一个数组传入,而 call 则作为 call 的参数传入(从第二个参数开始)。
另外一个结合前面定义的 Quo 构造器调用模式使用的例子:

1
2
3
4
5
var statusObject = {
status: 'A-OK'
};
var status = Quo.prototype.get_status.apply(statusObject);
console.log(status); // A-OK

在这里即使用了 apply 并将 statusObject 作为 get_statusthis,结果即为 A-OK

Reflect.apply

ES6 标准中新增的 Reflect.apply 方法允许通过指定的参数列表发起对目标函数的调用。相比于前面的 apply 调用模式,它更清晰明了、简洁易懂。用法是

1
Reflect.apply(target, thisArgument, argumentsList)

接受的三个参数分别是目标函数,绑定的 this 参数和调用的参数列表。


参考文章:
Javascript 的函数调用与 this
JavaScript 函数调用

拓展阅读:
闭包深入:让你分分钟理解 JavaScript 闭包
一道 javascript 面试题求教 - V2EX