scope
(作用域) 和context
(上下文) 是一个容易迷糊的地方。请参考: Javascript Context和Scope的一些学习总结推荐阅读: 深入浅出ES6(1~10)系列
会用一门语言来写程序,并不代表就能正确地理解和使用该语言。当然, JavaScript 也是如此。尽管JS(JavaScript 的简称)是一门很容易上手的语言, 但里面其实有许多细节, 初学者可能并不了解, 即使是经验丰富的JS高手也偶尔会掉到坑里去。
很多有经验的程序员对 this
在 JS 内部是如何运作的也是一头雾水。通俗点讲, this
只是一个引用别名(referencing alias) - 这个别名只知道当前指向的那个对象, 而这也是最棘手的地方。
本文为你理清思路,并介绍 this
关键字的内部运作原理。
那么, this
是个什么鬼?
简而言之, this
是一个特殊的标识符关键字 —— 在每个 function 中自动根据作用域(scope) 确定, 指向的是此次调用的 “所有者,owner”。但要完全搞定这个问题, 我们需要回答两个关键问题:
this
是如何创建的?
每调用一次 JavaScript 函数时,都会创建一个新的对象, 其中的信息包括: 传入了哪些参数, 函数是如何调用(invoked)的, 函数是在哪里被调用(called)的,等等。该对象中还有一个重要的属性是 this
引用, 函数是哪个对象的方法,this
就会自动绑定到该对象。
提示: 详情请参考 ECMAScript 语言规范 §10.4.3节 以及其中的相关链接。
var car = {
brand: "Nissan",
getBrand: function(){
console.log(this.brand);
}
};
car.getBrand();
// output: Nissan
在这个例子中, 使用的是 this.brand
, 这是 car
对象的引用。所以此时, this.brand
等价于 car.brand
。
this
指向的是谁
在所有 function 中, this
的值都是根据上下文(context, 函数在调用时刻所处的环境)来决定的。this
的作用域(scope) 与函数定义的位置没有关系, 而是取决于函数在哪里被调用( where they are called from ;i.e. the context)。
每一行JavaScript代码都是在执行上下文(execution context)中运行的。this
指向的对象在每次进入新的执行上下文后是固定的, 直到跳转(shifted)到另一个不同的上下文才发生改变。决定执行上下文(以及 this
的绑定)需要我们去找出调用点(call-site), 调用点即函数在代码中调用的位置。
让我们看下面的示例:
var brand = 'Nissan';
var myCar = {brand: 'Honda'};
var getBrand = function() {
console.log(this.brand);
};
myCar.getBrand = getBrand;
myCar.getBrand();
// output: Honda
getBrand();
// output: Nissan
虽然 myCar.getBrand()
和 getBrand()
指向的是同一个函数, 但其中的 this
是不同的,因为它取决于getBrand()
在哪个上下文中调用。
我们已经知道, function 是哪个对象的方法, 在函数中, this
就绑定到这个对象。
上面的代码里,第一次函数调用对应的是 myCar
对象, 而第二次对应的是 window
【 此时 getBrand()
等价于 window.getBrand()
】。因此,不同的上下文产生的是不同的结果。
调用上下文
下面,让我们看看不同的上下文中this
的指向。
全局作用域(Global Scope)
所有的JavaScript 运行时都有唯一的全局对象(global object)。在浏览器中,全局对象是window
。而在Node.js里面是 global
。
在全局执行上下文中(任何函数之外的代码), this
指向的是全局对象, 不管是否在严格模式下(strict mode
)。
局部作用域(Local Scope)
在函数中, this
取决于函数是怎么调用的。分三种情况:
1. 在简单函数调用(Simple Function Call)中使用this
第一种情形是直接调用一个独立的函数。
function simpleCall(){
console.log(this);
}
simpleCall();
// output: the Window object
在这种情况下,this
值没有被 call 设置。因为代码不是运行在严格模式下, this
又必须是一个对象, 所以他的值默认为全局对象。
如果是在严格模式(strict mode)下, 进入执行上下文时设置为什么值那就是什么值。如果没有指定, 那么就一直是undefined
, 如下所示:
function simpleCall(){
"use strict";
console.log(this);
}
simpleCall();
// output: undefined
2. 在对象的方法(Object’s Method)中使用 this
将函数保存为对象的属性, 这样就转化为一个方法, 可以通过对象调用这个方法。当函数被当成对象的方法来调用时, 里面的 this
值就被设置为调用方法的对象。
var message = {
content: "I'm a JavaScript Ninja!",
showContent: function() {
console.log(this.content);
}
};
message.showContent();
// output: I'm a JavaScript Ninja!
showContent()
是 message
对象的一个方法, 所以此时 this.content
等价于 message.content
。
3. 在构造函数(Constructor Function)中使用 this
我们可以通过 new
操作符来调用函数。在这种情况下,这个函数就变成了构造函数(也就是一个对象工厂)。与前面讨论的简单函数调用和方法调用不同, 构造函数调用会传入一个全新的对象作为 this
的值, 并且隐式地返回新构造的这个对象作为结果【简言之, 新构造对象的内存是 new 操作符分配的, 构造函数只是做了一些初始化工作】。
当一个函数作为构造器使用时(通过 new
关键字), 它的 this
值绑定到新创建的那个对象。如果没使用 new
关键字, 那么他就只是一个普通的函数, this
将指向 window
对象。
function Message(content){
this.content = content;
this.showContent = function(){
console.log(this.content);
};
}
var message = new Message("I'm JavaScript Ninja!");
message.showContent();
// output: I'm JavaScript Ninja!
在上面的示例中, 有一个名为 Message()
的构造函数。通过使用 new
操作符创建了一个全新的对象,名为message
。同时还通传给构造函数一个字符串, 作为新对象的content
属性。通过最后一行代码中可以看到这个字符串成功地打印出来了, 因为 this
指向的是新创建的对象, 而不是构造函数本身。
如何正确地使用 this
在本节中,我们将学习一些决定 this
行为的内部机制。
在JavaScript中,所有的函数都是对象, 因此函数也可以有自己的方法。所有的函数都有的两个方法, 是 apply()和 call(). 。我们可以通过这两个方法来改变函数的上下文, 在任何时候都有效, 用来显式地设置 this
的值。
apply()
方法接收两个参数: 第一个是要设置为 this
的那个对象, 第二个参数是可选的,如果要传入参数, 则封装为数组作为 apply()
的第二个参数即可。
call()
方法 和 apply()
基本上是一样的, 除了后面的参数不是数组, 而是分散开一个一个地附加在后面。
下面来演练一下:
function warrior(speed, strength){
console.log(
"Warrior: " + this.kind +
", weapon: " + this.weapon +
", speed: " + speed +
", strength: " + strength
);
}
var warrior1 = {
kind: "ninja",
weapon: "shuriken"
};
var warrior2 = {
kind: "samurai",
weapon: "katana"
};
warrior.call(warrior1, 9, 5);
// output: Warrior: ninja, weapon: shuriken, speed: 9, strength: 5
warrior.apply(warrior2, [6, 10]);
// output: Warrior: samurai, weapon: katana, speed: 6, strength: 10
在这里,我们有一个工厂函数 warrior()
, 通过传入不同的战士对象创建不同类型的warrior(战士)。那么,在工厂函数里面, this
将指向我们通过call()
和/或 apply()
传入的对象。
在第一个函数调用中,我们使用call()
方法来将 this
设置为 warrior1
对象, 并传入需要的其他参数, 参数间用逗号分隔。在第二个函数调用中, 其实都差不多, 只是传入的是 warrior2
对象, 并将必要参数封装为一个数组。
除了call()
和 apply()
以外, ECMAScript 5还增加了 bind() 方法, 在调用一个函数或方法时也可以通过bind
方法来绑定 this
对象。让我们看下面的例子:
function warrior(kind){
console.log(
"Warrior: " + kind +
". Favorite weapon: " + this.weapon +
". Main mission: " + this.mission
);
}
var attributes = {
weapon: "shuriken",
mission: "espionage"
};
var ninja = warrior.bind(attributes, "ninja");
ninja();
// output: Warrior: ninja. Favorite weapon: shuriken. Main mission: espionage
在这个例子中, bind()
方法的使用方式还是类似的, 但 warrior.bind()
创建了一个新的函数(方法体和作用域跟warrior()
一样),并没有改动原来的 warrior()
函数。新函数的功能和老的一样, 只是绑定到了attributes
对象。
【
bind
方法和call
,apply
的区别在于,bind()
之后函数并没有执行, 可以传给其他函数, 在某个适当的时机再调用 】
总结
So, 与 this
有关的知识差不多就是这些。作为初学者,掌握这些你就足够来正确地使用它了, 对自己多点信心! 当然, 在使用的过程中也可能有一些问题会困扰你, 那么, 欢迎阅读下一篇文章 掌握JS中的“this
” (二)。
推荐阅读
阮一峰老师的: ECMAScript 6入门
CNBlog : Javascript Context和Scope的一些学习总结
infoq: 深入浅出ES6(1~10)系列
babeljs-ES6: https://babeljs.io/repl/
相关链接
原文链接: Revealing the Inner Workings of JavaScript’s “this” Keyword
原文日期: 2015年05月01日
翻译日期: 2015年09月17日