this 关键字是 JavaScript 中最复杂的机制之一,被自动定义在所有函数的作用域中。
常见误解
-
指向自身
思考以下代码:
function foo(num) { console.log( "foo: " + num ); // 记录 foo 被调用次数 this.count++; } foo.count = 0; var i; for( i=0; i<10; i++) { if(i > 5) foo(i); } // foo: 6 // foo: 7 // foo: 8 // foo: 9 // foo 被调用了多少次 console.log( foo.count ); // 0 ----> what!?console.log 输出了4条语句,说明 foo(...) 确实被调用了四次,但是 foo.count 为0,说明指向自身的理解是错误的。
那为什么会是 0 呢,实际上上面这段代码创建了一个全局变量 count,它的值为 NAN
-
它的作用域
this 指向函数的作用域。这个问题有点复杂,因为在某种情况下它是正确的,但是在其他情况下确实错误的
需要明确的是, this 在任何情况下都不指向函数的词法作用域
思考以下代码:
function foo() { var a = 2; this.bar(); } function bar() { console.log( this.a ); } foo(); // ReferenceError: a is not defined它试图跨越边界,使用 this 来隐式应用函数的词法作用域(失败)
this 到底是什么
this 是在运行时绑定,并不是在编写时绑定的,它的上下文取决于函数调用的各种条件。this 的绑定与和声明的位置没有关系,只取决于函数的调用方式。
当一个函数被调用,会创造一个活动记录(上下文)。该记录会包含函数在哪里被调用(调用栈)、调用的方式、传入的参数等。this 就是这个记录的属性。接下来让我们学习一下如何寻找函数的调用位置吧
this 调用位置
通常来说寻找调用位置就是寻找“函数被调用的位置”,可是有些编程模式可能会隐藏真正的调用位置,分析过程就不会那么简单。最重要的是分析调用栈。我们关心的调用位置就在当前正在执行的的函数的前一个调用中
代码演示如下:
function baz() { // 当前调用栈是: baz // 因此,当前调用位置是全局作用域 console.log( "baz" ); bar(); // <-- bar 的调用位置 } function bar() { // 当前调用栈是 baz -> bar // 因此,当前的调用位置在 baz 中 console.log( "bar" ) foo(); // <-- foo 的调用位置 } function foo() { // 当前调用栈是 baz -> bar -> foo // 因此, 当前调用位置在 baz 中 console.log( "foo" ); } baz(); // <-- baz 的调用位置this 的绑定规则
-
默认绑定
无法应用其他规则是的默认规则
代码如下:
function foo() { console.log( this.a ); } var a = 2; foo(); // 2foo() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,因此 this 指向全局
但是,如果使用严格模式( strict mode ),不能将全局对象用于默认绑定,因此 this 会绑定到undefined
代码如下:
function foo() { "use strict"; console.log( this.a ); } var a = 2; foo(); // TypeError: this is undefined -
隐式绑定
调用位置是否有上下文,或者说是否被某个对象拥有或者包含
代码如下:
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo } obj.foo(); // 2当 foo() 被调用时,它的前面有对 obj 的引用,从而有了上下文对象,隐式绑定规则会将 this 绑定到这个上下文对象中。因此 this 被绑定到 obj, this.a === obj.a
注意:对象属性引用链只有上一层或者说最后一层在调用位置中起作用,举例来说:
function foo() { console.log( this.a ); } var obj2 = { a: 42, foo: foo }; var obj1 = { a: 2, obj2: obj2 }; obj1.obj2.foo(); // 42隐式丢失
一个最常见的 this 绑定问题: 被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定在全局对象上或者 undefined 上(严格模式下)。
思考以下代码:
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo } var bar = obj.foo; // 函数别名 var a = 'oops, global'; // a 是全局对象属性 bar(); // "oops, global"上述代码中,虽然 bar 是 obj.foo 的一个引用,但实际上引用的是 foo 函数本身,因此此时的 bar() 其实是一个不带任何修饰的函数引用,因此应用了默认绑定。
思考练习代码:
function foo() { console.log( this.a ); } function doFoo(fn) { fn(); } var obj = { a: 1, foo: foo } var a = "oops, global"; doFoo( obj.foo ); // 会输出什么? 欢迎在评论区讨论 -
显示绑定
在分析隐式绑定时,我们必须在一个对象内部包含一个执行函数的属性, 并通过这个属性间接引用函数,从而把 this 间接绑定到这个对象上
如果不想在对象内部包含函数的引用,而想在某个对象上强制调用函数,怎么办?
JavaScript 中函数有一些有用的特性可以解决这个问题:call(...) 和 apply(...) 方法。他们的第一个参数是一个对象,给 this 准备的,接着在调用时将其绑定到 this。 因为可以直接指定 this 的绑定对象,因此称之为显示绑定
示例代码:
function foo() { console.log( this.a ); } var obj = { a: 2 }; foo.call( obj ); // 2如果 call(...) 传入的是一个原始值(字符串、布尔、数字)来当作 this 的绑定对象,这个原始值会被转化成为它的对象形式。这通常被称为“装箱”
可是,显示绑定仍然无法解决之前提出的绑定丢失问题
- 硬绑定
显示绑定的一个变种可以解决这个问题
思考以下代码:
function foo() { console.log( this.a ); } var obj = { a: 2 } var bar = function() { foo.call( obj ); } bar(); // 2 setTimeout( bar, 100 ); // 2 // 硬绑定的 bar 不可能再修改它的 this bar.call( window ); // 2我们创建了函数 bar() ,并在它的内部手动调用了 foo.call( obj ),因此强制把 foo 的 this 绑定到 obj。之后无论如何调用 bar,它总会手动在 obj 上调用 foo。因此我们称之为硬绑定
硬绑定的典型应用场景就是创建一个包裹函数,负责接收参数并返回值:
function foo(something) { console.log( this.a, something ); return this.a + something; } var obj = { a: 2 }; var bar = function() { return foo.apply( obj, arguments ); }; var b = bar(3); // 2 3 console.log( b ); // 5
-
-
new 绑定
在传统的面向类的语言中, “构造函数” 是类中的一些特殊方法,使用 new 初始化类时会调用类中的构造函数。通常形式为:
something = new Myclass(...);
JS 中也有一个 new 操作符,实际上和面向类的语言完全不同。在 JS 中,构造函数只是一些使用 new 操作符时被调用的函数。它们并不属于某个类,也不会实例化一个类。实际上它们甚至不能说是一种特殊的函数类型,它们只是被 new 操作符调用的普通函数而已。
ES5.1 这样描述 Number(...) 作为构造函数的行为: 当 Number在 new 表达式中被调用时,它是一个构造函数:它会初始化新创建的对象。
使用 new 来调用函数,会自动执行以下操作
- 创建(构造)一个全新的对象
- 这个新对象会被执行 [[Prototype]] 连接
- 这个新对象会绑定到函数调用的 this
- 如何函数没有返回其他对象, new 表达式中的函数调用会自动返回这个新对象
思考以下代码:
function foo(a) { this.a = a; } var bar = new foo(2); console.log( bar.a ); // 2使用 new 调用 foo(..) 时,构造一个新对象 bar 并把它绑定到 foo(...) 调用的 this 上。
绑定规则的优先级
new 绑定 > 显示绑定 > 隐士绑定 > 默认绑定
想要了解具体证明细节可翻阅《你不知道的JavaScript上》
ES6 箭头函数
箭头函数并不是使用 function 关键字定义的,而是使用 " => " 操作符定义的,不适用上述四种规则,而是根据外层(函数或者全局)作用域来决定 this
实例代码:
function foo() { // 返回一个箭头函数 return (a) => { // this 继承 foo() console.log( this.a ); }; } var obj1 = { a: 2 }; var obj2 = { a: 3 }; var bar = foo.call( obj1 ); bar.call( obj2 ); // 2, 而不是3!foo() 内部创建的箭头函数会捕获调用时 foo() 的 this。由于 foo() 的 this 绑定到 obj1, bar(引用箭头函数)的 this 也会被绑定到 obj1,箭头函数绑定无法被修改。