全面解析this的指向

1. 回顾上一节的知识点

1. 执行上下文分类: 全局执行上下文;函数执行上下文; eval执行上下文  
2. 执行上下文的创建阶段包括哪几步:1.binding this; 2. 创建词法环境 3. 创建变量环境  
3. ES6中,词法环境和变量环境的区别在于前者用来保存函数声明和let、const声明的变量;后者用来保存var声明的变量。
4. JavaScript中,基本数据类型存放在栈中,引用类型存放在堆中。
5. 内存回收:全局变量很难判断什么时候回收,局部变量用完就回收,因此尽量避免使用全局变量。
6. 内存回收算法: 引用计数(现代浏览器已经不再使用);标记清除法(现代浏览器基本上在使用)。
7. 内存泄漏:不用的内存不释放就会产生内存溢出--内存释放:赋值为null,##。

`this` 关键字是 `JavaScript` 中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。但是即使是非常有经验的 `JavaScript` 开发者也很难说清它到底指向什么。
在掌握this之前,我们首先要掌握以下两个概念  
* 作用域
* 执行上下文
执行上下文分类: 全局执行上下文;函数执行上下文; eval执行上下文  
执行上下文的创建阶段包括哪几步:1.binding this; 2. 创建词法环境 3. 创建变量环境
复制代码

2.对于this错误的认识

2.1 误解一:this指向自身

第一种常见的误解就是this指向函数自身。

```js
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 );
 }
}

console.log(foo.count);
```

分析:按照我们正常的逻辑进行分析,for循环执行了4次,按理来说,foo.count应该输出4.但是实际上,foo.count输出的是0.为什么?因为在上面那段代码中实际上创建了一个全局的count,这个全局的count和foo.count不一样。如果想要让foo.count输出的内容为我们预期中的4,应该进行怎样的修改?
复制代码

2.2 误解二:this指向函数的作用域

第二种常见的误解就是this指向函数的作用域。

```js
function foo() {
 var a = 2;
 this.bar();
}
function bar() {
 console.log( this.a );
}
foo(); // undefined

```

一起来挑刺:上面这段代码有哪些地方写的不好?
首先,这段代码试图通过 this.bar() 来引用 bar() 函数。如果要引用bar函数,直接引用就可以。
此外,编写这段代码的开发者还试图使用 this 联通 foo() 和 bar() 的词法作用域,从而让bar() 可以访问 foo() 作用域里的变量 a。这是不可能实现的,你不能使用 this 来引用一个词法作用域内部的东西。
复制代码

2.3 this到底是什么?

词法作用域的执行环境是在编写代码时就指定了的,而`this`是在运行时进行绑定的,`this`的绑定与位置没有任何关系,只取决于函数的调用方式。

上一节说的函数在被调用的时候,会创建执行上下文,这个执行上下文里包含了`this`的绑定、词法环境和变量环境,其中,词法环境有两个部分:环境记录和对外部环境的引用。
环境记录包括了函数声明和变量存储的实际位置,而对外部环境的引用则显示其可以引用的外部词法环境。  

**`this`实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。**
复制代码

3. 深入理解this

3.1 调用位置

在理解 `this` 的绑定过程之前,首先要理解调用位置:调用位置就是函数在代码中被调用的位置(而不是声明的位置)。只有仔细分析调用位置才能回答这个问题:这个 `this` 到底引用的是什么?通常来说,寻找调用位置就是寻找“函数被调用的位置”,但是做起来并没有这么简单,因为某些编程模式可能会隐藏真正的调用位置。  
最重要的是要分析执行栈(我们在上一节也讲过,也叫调用栈,具有 LIFO(后进先出)结构,用于存储在代码执行期间创建的所有执行上下文。  首次运行JS代码时,会创建一个全局执行上下文并Push到当前的执行栈中。每当发生函数调用,引擎都会为该函数创建一个新的函数执行上下文并Push到当前执行栈的栈顶。根据执行栈LIFO规则,当栈顶函数运行完成后,其对应的函数执行上下文将会从执行栈中Pop出,上下文控制权将移到当前执行栈的下一个执行上下文)。我们关心的调用位置就在当前正在执行的函数的前一个调用中。

```js
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 的调用位置
```

注意我们是如何(从调用栈中)分析出真正的调用位置的,因为它决定了 `this` 的绑定。
复制代码

3.2 绑定规则

3.2.1 默认绑定
```js
function foo() {
    console.log( this.a );
}
var a = 2;
foo(); //  <-- foo()函数是在这里被调用的,调用是不带任何修饰符.
```

```js
function foo() {
   "use strict" // 函数运行在严格模式时,this会绑定到undefined.
    console.log( this.a );
}
var a = 2;
foo(); //undefined
```

* 没有应用其他任何规则的默认规则,是独立的函数调用(不带任何修饰符的函数引用进行调用)。
* 当函数运行在严格模式下时,全局对象无法使用默认绑定,`this`会绑定到`undefined`,只用当函数运行在非严格模式下时,默认绑定才能绑定到全局对象。
复制代码
3.2.2 隐式绑定
隐式绑定:另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含,不过这种说法可能会造成一些误导。

```js
function foo() {
 console.log( this.a );
}
var obj = {
 a: 2,
 foo: foo
};
obj.foo(); // <--函数在这里被调=电泳,调用位置有obj上下文
```

在上述代码中,调用位置使用`obj`上下文来引用函数,我们可以认为函数被调用时`obj`对象“拥有”或者“包含”它,但是严格来说这个函数不属于`obj`对象。

无论你如何称呼这个模式,当 `foo()` 被调用时,它的落脚点确实指向 `obj` 对象。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 `this`绑定到这个上下文对象。因为调
用 `foo()` 时 `this` 被绑定到 `obj`,因此 `this.a` 和 `obj.a` 是一样的。

##### 3.2.2.1隐式丢失

某些情况下,隐式绑定的函数会丢失绑定对象,此时将应用默认规则,也就是将`this`绑定到全局对象(非严格模式)或者`undefined`(严格模式)上。

```js
function foo() {
 console.log( this.a );
}
var obj = {
 a: 2,
 foo: foo
};
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a 是全局对象的属性
bar();
```

我们对上段代码进行分析,虽然 `bar` 是 `obj.foo` 的一个引用,但是实际上,它引用的是 `foo` 函数本身,因此此时的`bar()` 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。下面这张图有助于我们理解。  
![_20190417112206.png](http://172.16.14.82:9000/ibed/2019/04/16/47f3d46928e64c95add95e256ee0fa3a.png)

还有一种情况,关于传参:

```js
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.2.3 显示绑定
就像我们刚才看到的那样,在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把 `this` 间接(隐式)绑定到这个对象上。那么如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?  
在这里我们要介绍两个js中常用的方法: `apply(...)`和`call(...)`.

`apply`和`call`的简单介绍

* apply
语法:func.apply(thisArg, [argsArray])  
`thisArg`--可选的。在 `func` 函数运行时使用的 `this` 值。请注意,**`this`可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 `null` 或 `undefined` 时会自动替换为指向全局对象,原始值会被包装**。  
`argsArray`--可选的。一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 `func` 函数。如果该参数的值为 `null` 或  `undefined`,则表示不需要传入任何参数。从ECMAScript 5 开始可以使用类数组对象。

* call
语法:fun.call(thisArg, arg1, arg2, ...)  
`thisArg`--在 fun 函数运行时指定的 `this` 值。需要注意的是,指定的 `this` 值并不一定是该函数执行时真正的 `this` 值,如果这个函数在非严格模式下运行,则指定为 `null` 和 `undefined` 的 this 值会自动指向全局对象(浏览器中就是 `window` 对象),同时值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的自动包装对象。  
`arg1, arg2, ...`--指定的参数列表。

这里解释一下对象的封装和拆封的概念:  
* 封装:基本类型值没有.length和.toString()这样的属性和方法,需要通过封装对象才能访问;此时JavaScript 会自动为基本类型值包装(box 或者wrap)一个封装对象。
```js
var a = "abc";
a.length; // 3
a.toUpperCase(); // "ABC"
```
* 拆封:如果想要得到封装对象中的基本类型值,可以使用valueOf() 函数:
```js
var a = new String( "abc" );
var b = new Number( 42 );
var c = new Boolean( true );
a.valueOf(); // "abc"
b.valueOf(); // 42
c.valueOf(); // true
```
在需要用到封装对象中的基本类型值的地方会发生隐式拆封。

```js
function foo (a, b) {
console.log(this.a);
console.log(a)
console.log(b)
}
```

`apply`和`call`的区别:`apply`接收的是一个包含多个参数的列表,而`call`方法接收若干个参数的列表。
```js
let obj = {
    a: 'Hello'
};
foo.call(obj, 'Tom', 'Jerry');
foo.apply(obj, ['Tom', 'Jerry']);
```

使用`call`和`apply`可以强制把`this`绑定到你想要绑定的对象上。但是仍然无法解决上述提到的`this`丢失的问题。

以下两个方法可以解决此问题。

##### 3.2.3.1硬绑定

```js
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`。这种绑定是一种显式的强制绑定,因此我们称之为硬绑定。

硬绑定的典型应用场景就是创建一个包裹函数,传入所有的参数并返回接收到的所有值:

```js
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
```

或者:

```js
function foo(something) {
   console.log( this.a, something );
   return this.a + something;
}
// 简单的辅助绑定函数
function bind(fn, obj) {
   return function() {
   return fn.apply( obj, arguments );
   };
}
var obj = {
   a:2
};
var bar = bind( foo, obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5
```

看到这里,熟悉`bind()`方法的人应该会很开心了,这难道不就是ES5中的`Function.prototype.bind`方法么!  
再简单复习以下`bind()`方法:  
语法:function.bind(thisArg[, [arg1[, arg2[, ...]]])  
`thisArg`--调用绑定函数时作为`this`参数传递给目标函数的值。 如果使用`new`运算符构造绑定函数,则忽略该值。当使用`bind`在`setTimeout`中创建一个函数(作为回调提供)时,作为`thisArg`传递的任何原始值都将转换为`object`。如果`bind`函数的参数列表为空,执行作用域的`this`将被视为新函数的`thisArg`。
`arg1, arg2, ...`--当目标函数被调用时,预先添加到绑定函数的参数列表中的参数。

`bind`与`apply`和`call`的区别: `bind`返回的是一个绑定上下文的函数,后两者是直接执行了函数。

```js
function foo(something) {
   console.log( this.a, something );
   return this.a + something;
}
var obj = {
   a:2
};
var bar = foo.bind( obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5
```

`bind(..)` 会返回一个硬编码的新函数,它会把参数设置为 `this` 的上下文并调用原始函数。
复制代码
3.2.3.2 API调用的上下文
第三方库的许多函数,以及 JavaScript 语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和 `bind(..)` 一样,确保你的回调函数使用指定的 `this`。
```js
function foo(el) {
console.log( el, this.id );
}
var obj = {
id: "awesome"
};
// 调用foo(..) 时把this 绑定到obj
[1, 2, 3].forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome
```
复制代码
3.2.4 new绑定
在《JavaScript对象与原型》中,我们讲过,通过new调用构造函数时,会发生以下几步:

1. 创建(或者说构造)一个全新的对象。
2. 创建他的构造函数。
3. 这个新对象会被执行 [[ 原型 ]] 连接。
4. 这个新对象会绑定到函数调用的 `this`。
5. 如果函数没有返回其他对象,那么 `new` 表达式中的函数调用会自动返回这个新对象。

> 这里大家可以自行回顾如何手写一个`new`实现。
```js
function create() {
   let obj = new Object();
   let Con = [].shift.call(arguments);
   obj.__proto__ = Con.protoType;
   let ret = Con.apply(obj,arguments);
   return typeof ret === 'object'? ret : obj;
}
```
使用这个手写的new:

```js
function Person(name) {
   this.name = name;
   return 'new person';
}
let person = new Foo('Lucy');
person.name; // Lucy
let person = create(Person, 'Lucy');
person.name; //Lucy
```
思考下面的代码:

```js
function foo(a) {
    this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
```

使用`new` 来调用 `foo(..)` 时,我们会构造一个新对象并把它绑定到 `foo(..)` 调用中的 `this`上。`new` 是最后一种可以影响函数调用时 `this` 绑定行为的方法,我们称之为 `new` 绑定。
复制代码

4. 优先级

上述我们一共给出了4中绑定this的规则,那么需要考虑到这四种规则到底谁的优先级更高呢?
优先级比较:
new绑定 > 显式绑定 > 隐式绑定 > 默认绑定
```
1. 函数是否在new 中调用(new 绑定)?如果是的话this 绑定的是新创建的对象。
var bar = new foo()
2. 函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是
指定的对象。
var bar = foo.call(obj2)
3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上
下文对象。
var bar = obj1.foo()
4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到
全局对象。
var bar = foo()

```
<!-- > 这里留下一道实践题,大家可以自己写例子来比较上述四种绑定方式的优先级。

给个例子比较显式绑定和隐式绑定:

```js
function foo(something) {
   console.log( this.a, something );
   return this.a + something;
}
var obj1 = {
   a: 2;
   foo: foo
};
var obj2 = {
   a: 3;
   foo: foo
};
var a = obj1.foo()
var bar = foo.bind( obj2 );
var b = bar( 3 ); // 2 3
console.log( b ); // 5
``` -->
复制代码

5.绑定例外

5.1 被忽略的this

上面在简单介绍`apply`、`call`、`bind`的时候有说过,如果第一个参数传的是`null`或者`undefined`,那么在非严格模式下,`this`会绑定到全局变量`window`。  
在展开数组中的元素、函数柯里化时参数会传null。
```js
function foo(a,b) {
 console.log( "a:" + a + ", b:" + b );
}

foo.apply( null, [2, 3] ); // a:2, b:3

// 使用 bind(..) 进行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3
```

然而,总是使用 `null` 来忽略 `this` 绑定可能产生一些副作用。如果某个函数确实使用了`this`(比如第三方库中的一个函数),那默认绑定规则会把 `this`绑定到全局对象(在浏览器中这个对象是 window),这将导致不可预计的后果(比如修改全局对象)。  
> 我们一般建议使用`Object.create(null)`来代替直接使用`null`,因为任何对于 `this` 的使用都会被限制在这个空对象中,不会对全局对象产生任何影响。
复制代码

5.2 间接引用

```js
function foo() {
 console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
(p.foo = o.foo)(); // 2
```
赋值表达式p.foo = o.foo 的返回值是目标函数的引用,因此调用位置是foo() 而不是p.foo() 或者o.foo()。根据我们之前说过的,这里会应用默认绑定。
> 注意:对于默认绑定来说,决定 `this` 绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,`this` 会被绑定到 `undefined`,否则`this `会被绑定到全局对象。
复制代码

5.3 软绑定

在上面我们提到了硬绑定,那为什么在这里又要提出软绑定呢?
硬绑定的缺点主要变现在一旦使用硬绑定指定了`this`,再无法用隐式绑定改变`this`的指向,大大降低了灵活性。在这种情况下,提出了软绑定。
软绑定:给默认绑定指定一个全局对象和 `undefined` 以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改 `this` 的能力。

```js
function foo() {
  console.log("name: " + this.name);
}
var obj = { name: "obj" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" };
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj
obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2 <---- 看!!!
fooOBJ.call( obj3 ); // name: obj3 <---- 看!
setTimeout( obj2.foo, 10 );
// name: obj <---- 应用了软绑定
```
复制代码

6.箭头函数

箭头函数不适用于以上几条绑定规则,箭头函数的`this`的遵从词法作用域,是根据外层作用域来决定`this`。但是外层函数如果不是箭头函数,那么照样可以应用上述的四种规则。所以从某种情况下来说,可以理解成箭头函数也是“动态绑定的”。

```js
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  


<!--思考题:-->
<!--依次给出console.log输出的数值:-->
<!--```js-->
<!--var num = 1;-->
<!--var myObject = {-->
<!--   num: 2,-->
<!--   add: function() {-->
<!--      this.num = 3;-->
<!--      (function() {-->
<!--         console.log(this.num);-->
<!--         this.num = 4;-->
<!--      })();-->
<!--      console.log(this.num);-->
<!--   },-->
<!--   sub: function() {-->
<!--      console.log(this.num);-->
<!--   }-->
<!--}-->
<!--myObject.add();-->
<!--console.log(myObject.num);-->
<!--console.log(num);-->
<!--var sub = myObject.sub;-->
<!--sub();-->
<!--```-->

<!--看答案吧:-->
<!--```js-->
<!--var num = 1;-->
<!--var myObject = {-->
<!--   num: 2,-->
<!--   add: function() {-->
      this.num = 3; // myObject.add()为隐式调用,所以this指向调用的上下文对象myObject,所以myObject.num=3.
<!--      (function() {-->
         console.log(this.num);  //立即执行函数,此时应用的是默认绑定规则,因此此时的this指向全局对象window.
         this.num = 4; // window.num=4
<!--      })();-->
      console.log(this.num); // 此时的myObject.num 为3.
<!--   },-->
<!--   sub: function() {-->
      console.log(this.num); // 应用默认规则,此时window.num为4.
<!--   }-->
<!--}-->
<!--myObject.add();-->
<!--console.log(myObject.num);-->
console.log(num); // 此时的全局的num值为4.
<!--var sub = myObject.sub;-->
sub(); // 这是函数的实际调用位置,没有任何修饰符,所以应用默认规则.
<!--```-->

参考:  
《你不知道的js--上卷》 --Kyle Simpson  
《MDN Web Docs》 --[mdn](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function)复制代码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值