本章借鉴《你不知道的javascript(上卷)》
1、关于this
this 关键字是 JavaScript 中最复杂的机制之一。它是一个很特别的关键字,被自动定义在
所有函数的作用域中。但是即使是非常有经验的 JavaScript 开发者也很难说清它到底指向
什么。
任何足够先进的技术都和魔法无异。 ——Arthur C. Clarke
实际上,JavaScript 中 this 的机制并没有那么先进,但是开发者往往会把理解过程复杂化,
毫无疑问,在缺乏清晰认识的情况下,this 对你来说完全就是一种魔法。
1.1、为什么要用this
-
使用this和显示传入代码:
-
使用this
function identify() { return this.name.toUpperCase(); } function speak() { var greeting = "Hello, I'm " + identify.call( this ); console.log( greeting ); } var me = { name: "Kyle" }; var you = { name: "Reader" }; speak.call( me ); // Hello, I'm KYLE speak.call( you ); // Hello, I'm READER
-
显式传入
function identify(context) { return context.name.toUpperCase(); } function speak(context) { var greeting = "Hello, I'm " + identify( context ); console.log( greeting ); } var me = { name: "Kyle" }; var you = { name: "Reader" }; speak( me ); // Hello, I'm KYLE speak( you ); // Hello, I'm READER
-
阅读两端代码可以看出
- 使用this,api设计会更简洁并且易于复用
- 阅读性的话,觉得显示传入并没有太大优势
- 当我们代码更加复杂的时候,显式传递上下文对象会让代码变得越来越混乱,使用 this 则不会这样。(想想我们的vue2代码如果使用显示传入会变成什么样!!!)
-
1.2、误解
-
1)、指向自身
-
人们很容易把 this 理解成指向函数自身,这个推断从英语的语法角度来说是说得通的。
-
那么为什么需要从函数内部引用函数自身呢?常见的原因是递归(从函数内部调用这个函
数)或者可以写一个在第一次被调用后自己解除绑定的事件处理器。
-
举个例子,this并不指向函数本身
function foo() { console.log(this) } foo() // Window
-
修正this指向本身
function foo() { console.log(this) } foo.call(foo) // ƒ foo() { // console.log(this) // }
-
这里只是举例,稍后会详解this绑定规则,到时候再看这里会更清晰
-
-
2)、它的作用域
-
第二种常见的误解是,this 指向函数的作用域。这个问题有点复杂,因为在某种情况下它
是正确的,但是在其他情况下它却是错误的。
-
阅读下面代码辅助理解下
// this var a = 2; function foo() { this.bar(); } function bar() { console.log( this.a ); } foo(); // 2
// 词法作用域 var a = 2; function foo() { bar(); } function bar() { console.log( a ); } foo(); // 2
- 上述两段代码,结果相同。也不解读,看完this绑定后可以回来缕一缕。
// this var a = 6 function foo() { var a = 2; function baz(){ console.log( this.a ); } baz() } foo(); // 6
// 词法作用域 var a = 6 function foo() { var a = 2; function baz(){ console.log( a ); } baz() } foo(); // 2
- 上面两段代码,结果不同。还是不解读,看完this绑定后可以回来缕一缕。
-
1.3、总结
- this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
- this 既不指向函数自身也不指向函数的词法作用域,你也许被这样的解释误导过,但其实它们都是错误的。
- this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。
2、this全面解析
2.1、调用位置
function baz() {
// 当前调用栈是:baz
// 因此,当前调用位置是全局作用域
console.log( "baz" );
bar(); // <-- bar 的调用位置
}
function bar() {
// 当前调用栈是 baz -> bar
// 因此,当前调用位置在 baz 中
console.log( "bar" );
foo(); // <-- foo 的调用位置
}
function foo() {
// 当前调用栈是 baz -> bar -> foo
// 因此,当前调用位置在 bar 中
console.log( "foo" );
}
baz(); // <-- baz 的调用位置
你可以把调用栈想象成一个函数调用链,就像我们在前面代码段的注释中所
写的一样。但是这种方法非常麻烦并且容易出错。另一个查看调用栈的方法
是使用浏览器的调试工具。绝大多数现代桌面浏览器都内置了开发者工具,
其中包含 JavaScript 调试器。就本例来说,你可以在工具中给 foo() 函数的
第一行代码设置一个断点,或者直接在第一行代码之前插入一条 debugger;
语句。运行代码时,调试器会在那个位置暂停,同时会展示当前位置的函数
调用列表,这就是你的调用栈。因此,如果你想要分析 this 的绑定,使用开
发者工具得到调用栈,然后找到栈中第二个元素,这就是真正的调用位置。
2.2、绑定规则
-
默认绑定、隐式绑定、显示绑定、new绑定
-
1)、默认绑定
- 函数调用时this会绑定全局对象。
- 但严格模式下,this会绑定到undefined
-
2)、隐式绑定
-
当函数作为某个对象的属性调用时,隐式绑定规则会把函数中的this绑定到调用当前函数的对象上。
-
只有直接调用函数的对象,才会影响this。
function foo() { console.log( this.a ); } var obj2 = { a: 42, foo: foo }; var obj1 = { a: 2, obj2: obj2 }; obj1.obj2.foo(); // 42
-
-
2.1)、隐式丢失
-
一个最常见的 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"
- 回调函数
function foo() { console.log( this.a ); } function doFoo(fn) { // fn 其实引用的是 foo fn(); // <-- 调用位置! } var obj = { a: 2, foo: foo }; var a = "oops, global"; // a 是全局对象的属性 doFoo( obj.foo ); // "oops, global"
-
-
3)、显式绑定
-
借助
call
、apply
和bind
将函数调用时的this绑定到指定对象上。function foo(){ console.log(this.a) } var obj = { a:'obj' } // call foo.call(obj); // obj // apply foo.apply(obj); // obj // bind foo.bind(obj)() // obj
-
-
4)、new绑定
-
使用new操作符调用函数时,会创建一个新对象,并把这个对象绑定到被调用函数的this。
function foo(a) { this.a = a; } var bar = new foo(2); console.log( bar.a ); // 2
-
2.3、优先级
-
默认绑定 < 隐式绑定 < 显式绑定 < new绑定
-
默认绑定最低。😄
-
隐式绑定 < 显式绑定
function foo() { console.log( this.a ); } var obj1 = { a: 2, foo: foo }; var obj2 = { a: 3, foo: foo }; obj1.foo(); // 2 obj2.foo(); // 3 obj1.foo.call( obj2 ); // 3 obj2.foo.call( obj1 ); // 2
-
隐式绑定 < new绑定
function foo(something) { this.a = something; } var obj1 = { foo: foo }; obj1.foo( 2 ); console.log( obj1.a ); // 2 var bar = new obj1.foo( 4 ); console.log( obj1.a ); // 2 console.log( bar.a ); // 4
-
显式绑定 < new绑定
function foo(something) { this.a = something; } var obj1 = {}; var bar = foo.bind( obj1 ); bar( 2 ); console.log( obj1.a ); // 2 var baz = new bar(3); console.log( obj1.a ); // 2 console.log( baz.a ); // 3
2.4、箭头函数
-
箭头函数并不是使用 function 关键字定义的,而是使用被称为“胖箭头”的操作符 => 定 义的。箭头函数不使this 的四种标准规则,而是根据外层(函数或者全局)作用域来决 定 this。
-
箭头函数的绑定无法被修改。(new 也不 行!)
2.5、总结
如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置。找到之后
就可以顺序应用下面这四条规则来判断 this 的绑定对象。
1、由 new 调用?绑定到新创建的对象。
2、由 call 或者 apply(或者 bind)调用?绑定到指定的对象。
3、由上下文对象调用?绑定到那个上下文对象。
4、默认:在严格模式下绑定到 undefined,否则绑定到全局对象。
ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定
this,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这
其实和 ES6 之前代码中的 self = this 机制一样。