关于this的知识点小总结
关于this的知识点小总结
本文是《你不知道的JavaScript》第二部分第一章和第二章的读书笔记。
一:为什么要使用this
首先来看下我们为什么要使用this。下面的代码显式地将上下文对象传递给函数,这种方式虽然很直观,但显式传递上下文对象会让代码变得越来越混乱。this提供了一种更优雅的方式来隐式地传递上下文对象,因此可以将 API 设计得更加简洁并且易于复用。
1 let me = { 2 name: "Chang Chang" 3 }; 4 let you = { 5 name: "Reader" 6 }; 7 8 function identify(context) { 9 let name = context.name.toUpperCase(); 10 console.log(name); 11 return name; 12 } 13 function speak(context) { 14 let greeting = "Hello, I'm " + identify(context); 15 console.log(greeting); 16 } 17 identify(you); // READER 18 speak(me); //hello, I'm CHANG CHANG
下面的代码展示了this的优点。通过调用函数的call方法,我们可以将一个对象绑定到一个函数的函数作用域中的this上,从而在不同的上下文对象(me 和 you)中复用函数 identify() 和 speak(),不用再针对每个对象编写不同版本的函数。
1 function identify() { 2 return this.name.toUpperCase(); 3 } 4 function speak() { 5 let greeting = "Hello, I'm " + identify.call(this); 6 console.log(greeting); 7 } 8 let me = { 9 name: "Chang Chang" 10 }; 11 let you = { 12 name: "Reader" 13 }; 14 identify.call(me); 15 identify.call(you); 16 speak.call(me); // Hello, I'm CHANG CHANG 17 speak.call(you); // Hello, I'm READER
二:对this的误解
首先要明确的是,在函数作用域中,this并不一定指向函数自身。在C++中,this是一个指向当前对象的const指针,通过this我们可以访问当前对象的所有成员。在JavaScript中,尽管函数也是对象,但其内部的this并不一定指向函数自身。下面的代码是一个典型的例子,我们可以看到在for循环执行完之后,foo.count依然等于0,这是因为在没有为foo显式或者隐式地指定this的情况下,this是指向全局对象global的。
1 function foo(num) { 2 console.log( "foo: " + num ); 3 // 记录 foo 被调用的次数 4 this.count++; 5 } 6 7 foo.count = 0; 8 9 let i; 10 for (i=0; i<10; i++) { 11 foo(i); 12 } 13 14 console.log(foo.count); // 0 15 console.log(global.count); // NAN, undefined++ = NAN
为了获得foo函数被调用的次数,我们可以在全局作用域创建一个count变量,然后在foo函数内部递增这个变量。这种方法利用了块级作用域,但无助于我们理解this的工作原理。
1 let count = 0; 2 3 function foo(num) { 4 console.log( "foo: " + num ); 5 // 记录 foo 被调用的次数 6 count++; 7 } 8 9 for (let i=0; i<10; i++) { 10 foo(i); 11 } 12 13 console.log(count); // 10
另一种获得foo函数调用次数的方法是使用其函数名foo代替this直接访问函数对象。这种方法同样回避了this, 完全依赖于变量foo的词法作用域。
1 function foo(num) { 2 console.log( "foo: " + num ); 3 // 记录 foo 被调用的次数 4 foo.count++; 5 } 6 7 foo.count = 0; 8 9 for (let i=0; i<10; i++) { 10 foo(i); 11 } 12 13 console.log(foo.count); // 10
上面两种方法通过JS的作用域机制回避了this,下面的代码则通过call方法将foo函数对象绑定了到了foo内部的this上。
1 function foo(num) { 2 console.log( "foo: " + num ); 3 // 记录 foo 被调用的次数 4 this.count++; 5 } 6 7 foo.count = 0; 8 9 for (let i=0; i<10; i++) { 10 foo.call(foo, i); 11 } 12 13 console.log(foo.count); // 10
除了误以为函数内部的this指向函数本身,另一种对this的误解是认为this指向函数的作用域。这里直接引用原书中的一段话作为解释:
“需要明确的是,this在任何情况下都不指向函数的词法作用域。在JavaScript 内部,作用域确实和对象类似,可见的标识符都是它的属性。但是作用域'对象' 无法通过 JavaScript代码访问,它存在于 JavaScript 引擎内部。” (引自《你不知道的JavaScript》)
三:this的绑定规则
1.默认绑定
在非严格模式下执行独立的函数调用时,会发生this的默认绑定,此时this会被绑定到全局对象上。可以把这条规则看作是无法应用其他规则时的默认规则。
1 function foo() { 2 console.log(this===global); 3 } 4 5 foo(); // true
上面的代码中对foo函数的调用没有任何修饰,因此只能使用默认绑定规则。另外,对于默认绑定的情况,在严格模式下this不会被绑定到全局对象上,此时this的值为undefined。
1 function foo() { 2 "use strict" 3 4 console.log(this===global); 5 } 6 7 foo(); // false
2.隐式绑定
如果在函数调用的位置有其他的上下文对象,或者说该函数被“包含”在其他对象中,此时会发生隐式绑定。
1 function foo() { 2 console.log(this.a); 3 } 4 5 let obj = { 6 a: 2, 7 foo: foo 8 }; 9 10 obj.foo(); // 2
在上面的代码中,foo函数被声明在全局作用域中,foo函数的引用被作为属性添加到了obj对象中。此时我们使用obj作为上下文对象调用foo函数,this会被绑定到obj对象上。但是,无论是直接在obj中定义foo函数,还是先定义foo函数再将其引用作为obj的属性,这个函数严格来说都不属于obj对象。另外需要注意的是,在对象属性引用链中只有最顶层的对象会影响this。在下面的代码中,this被绑定到了obj2上,obj1对this没有任何影响。
1 function foo() { 2 console.log(this.a); 3 console.log(this.b); 4 } 5 let obj2 = { 6 a: 42, 7 foo: foo 8 }; 9 10 let obj1 = { 11 a: 2, 12 b: 1, 13 obj2: obj2 14 }; 15 16 obj1.obj2.foo(); // 42 undefined
有时一个被隐式绑定的函数可能会丢失绑定的对象,此时会应用默认绑定的规则,将this绑定到全局对象或者undefined上,这取决于是否开启了严格模式。
1 function foo() { 2 console.log(this === obj); 3 } 4 var obj = { 5 foo: foo 6 }; 7 8 obj.foo(); // true 9 10 let bar = obj.foo; // 函数别名 11 bar(); // false
在上面的代码中,bar其实引用了foo函数本身,它只是foo函数的一个别名,和obj对象没有任何关系,因此在调用bar时不会把this绑定到obj上,此时应用了默认绑定规则。另一种更加隐蔽的this绑定丢失发生在传入回调函数时:
1 function foo() { 2 console.log(this === obj); 3 } 4 5 function doFoo(fn) { 6 fn(); 7 } 8 9 let obj = { 10 foo: foo 11 }; 12 13 obj.foo(); // true 14 doFoo(obj.foo); // false
由上面的代码可以看出,在将obj.foo作为参数传递给doFoo函数时,this的绑定又丢失了。这是因为参数的传递其实是将实参赋给形参的过程,因此this的绑定丢失了。
3.显式绑定
在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this隐式地绑定到这个对象上。显然,隐式绑定有一些繁琐,我们需要一种能显式地把this绑定到某个对象上的方法。函数的call方法和apply方法允许我们显式地指定this绑定的对象。call和apply方法的第一个参数是一个对象,这个对象会被绑定到被调用函数的this上。
1 let obj = { 2 a:2 3 }; 4 5 function foo() { 6 console.log(this.a); 7 console.log(this === obj); 8 } 9 10 foo.call(obj); // 2 true
通过 foo.call(..),我们可以在调用foo时把它的this强制绑定到obj上。尽管显式绑定简洁明了,但依然无法彻底解决绑定丢失的问题。为此,我们需要另一种被称为硬绑定的方法。通过函数的bind方法可以完成this的硬绑定。如果在foo上调用bind方法并传入对象obj,这个方法将返回一个新的函数,调用这个函数就如同调用obj的方法。
1 function foo(a) { 2 console.log(this.x + a); 3 } 4 5 let obj = { 6 x : 1 7 } 8 9 let foo2 = foo.bind(obj); 10 foo2(2); // 3 11 12 let obj2 = { 13 x : 10, 14 f : foo2 15 } 16 17 obj2.f(2); // 3。this依然指向obj
从上面的代码可以看出,即使将bind返回的函数作为某个对象的方法去调用也不会更改this的值,这避免了绑定丢失的问题。
4.new绑定
在C++中,构造函数是一类特殊的成员函数,它将在使用new初始化一个对象时被调用。但在JavaScript中,构造函数只是在使用new操作符时调用的函数,它们既不属于某个类,也不会实例化一个类。new操作符主要完成的工作包括:1.自动创建一个新对象。2.将新对象的原型设置为构造函数的prototype属性。3.将构造函数的this绑定到这个新对象上,并执行这个构造函数。4.如果构造函数没有返回其他对象,那么new 表达式中的函数调用会自动返回这个新对象。
1 function foo(a) { 2 this.a = a; 3 } 4 5 bar = new foo(2); 6 console.log(bar.a); // 2
从上面的代码可以看出,bar引用了new表达式返回的新对象。在执行new操作时这个对象被绑定到了foo函数的this上,foo函数初始化了这个对象的a属性。
四:this绑定的优先级
从this的绑定规则可以看出,this在函数执行的过程中被动态绑定(箭头函数除外,后面会介绍)。在判断this的指向时,我们应该分析函数的调用位置并确定应该使用哪条绑定规则。在代码中可能出现同一个调用位置可以应用多种绑定规则的情况,为了解决这个问题就必须给this的绑定规则设定优先级。
1 function foo() { 2 console.log(this.a); 3 } 4 5 let obj1 = { 6 a : 1, 7 foo : foo 8 } 9 10 let obj2 = { 11 a : 2, 12 foo : foo 13 } 14 15 obj1.foo.call(obj2); // 2 16 obj2.foo.call(obj1); // 1
从上面的代码可以看出,显式绑定的优先级高于隐式绑定的优先级,显式绑定重写了隐式绑定设置的this指向。
1 function foo(something) { 2 this.a = something; 3 } 4 5 let obj = { 6 foo : foo 7 } 8 9 obj.foo(1); 10 let bar = new obj.foo(2); 11 console.log(obj.a); // 1 12 console.log(bar.a); // 2
从上面的代码可以看出,new绑定的优先级高于隐式绑定的优先级。现在要确定显式绑定和new绑定的优先级哪个更高。从下面的代码中可以看到,函数的bind方法将创建一个新的包装函数,这个包装函数将忽略函数当前的this绑定,并将this绑定到bind方法的第一个参数上。如果通过new来调用bind返回的包装函数,则将返回一个新对象,这个新对象将被绑定到包装函数的this上。
1 function foo(a) { 2 this.a = a; 3 } 4 5 let obj1 = {}; 6 7 let bar = foo.bind(obj1); 8 bar(2); 9 console.log(obj1.a); // 2 10 11 let baz = new bar(3); 12 console.log(baz.a); // 3 13 console.log(obj1.a); // 2 14 console.log(baz === obj1); // false
五:this绑定的例外
1.被忽略的this
1 function foo(a, b) { 2 console.log("a = " + a + ", b = " + b); 3 } 4 5 foo(1, 2); // a = 1, b = 2 6 foo.call(null, 1, 2); // a = 1, b = 2 7 foo.apply(null, [1, 2]); // a = 1, b = 2 8 9 let bar = foo.bind(null, 1); // 柯里化 10 bar(2); // a = 1, b = 2
2.间接引用
1 function foo() { 2 console.log( this.a ); 3 } 4 5 var obj1 = { 6 a: 3, 7 foo: foo 8 } 9 10 var obj2 = { 11 a: 4 12 } 13 14 obj1.foo(); // 3 15 (obj2.foo = obj1.foo)(); // undefined,this被绑定到了全局对象上,此时相当于执行foo()
3.箭头函数
箭头函数根据词法作用域来决定this的指向,此外,箭头函数的this绑定无法被修改。
1 function foo() { 2 return (a) => { 3 console.log(this.a); 4 }; 5 } 6 7 let obj1 = { 8 a : 1 9 } 10 11 let obj2 = { 12 a : 2 13 } 14 15 func1 = foo.call(obj1); //箭头函数的this已经与obj1绑定了,无法被修改 16 func1(); // 1 17 18 func1.call(obj2); // 1

浙公网安备 33010602011771号