关于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
posted @ 2021-04-15 13:27  曹冲字仓舒  阅读(188)  评论(0)    收藏  举报