> 文档中心 > JavaScript -- this详解

JavaScript -- this详解

本博客基于《你不知道的JavaScript》这本书,是我对this相关问题的总结。

一、第一章:关于this

1.两个误解

● 误解一:this指向函数自身

函数中的this是根据函数调用时的上下文所决定的,该上下文不一定是函数自身。

举个例子

function a(){    console.log(this);    console.log(this.a);}a()console.log('分*************界****************线');a.bind(a)()

对于上面这个例子就很好的说明了误解一,a()函数this指向window,使用bind()函数绑定的this指向函数a本身。

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

this的指向是根据函数的执行顺序动态确定的,函数作用域是根据代码的书写顺序决定的(词法作用域)。什么是词法作用域?见我上一篇文章

举个例子

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

2.this究竟是什么?

this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。

当一个函数调用时,会创建一个上下文对象,这个上下文对象包括函数的调用栈、函数的方法、函数的参数等信息,this就是函数上下文中的一个属性。

二、第二章:this全面解析

1.调用位置

函数调用栈是指当函数嵌套调用时,函数会以栈的方式进行存储。函数的调用位置就在当前正在执行的函数的前一个调用中。

2.绑定规则

(1)默认绑定

默认绑定是当其他绑定规则不满足时,启用默认绑定规则。

举个例子

function a() {    console.log(this); // window}a()

注意:当函数内部或全局启用严格模式('use strict')的时候,默认绑定并不会绑定到window,会绑定到undefined。

举个例子

function foo1() {    "use strict";    console.log(this); // undefined}foo1(); "use strict";function foo2() {    console.log(this); // window}foo2(); 

(2)隐式绑定

隐式绑定是指函数是在哪个作用域进行调用的,函数的this就指向哪个作用域的this,也可以说是调用当前函数的前一个调用栈。

举个例子

function foo() {    console.log(this.a); // 1,this指向 obj2,foo函数的调用栈是 obj2    console.log(a) // 3,词法作用域(只与函数书写位置有关),函数书写在全局作用域}var a = 3;var obj2 = {    a: 1,    foo: foo};var obj1 = {    a: 2,    obj2: obj2};obj1.obj2.foo(); 

其实隐式绑定与默认绑定原理是相同的,默认绑定在全局作用域进行调用 f(),其实也是window.f()。

隐式丢失问题

指进行隐式绑定的函数this可能会丢失绑定对象,导致this指向window或undefined(严格模式)。

举个例子(赋值引起)

function a(){    console.log(this.num);}var obj = {    num:1,    a:a}var num = 2;var b = obj.a; // 赋值方式b(); // 2

这个例子中this就丢失了绑定对象obj。因为b指向a(b保存着a的引用),b()的调用并没有与obj发生联系,obj就好像是一个卖苹果(a的引用)的小贩,小贩把苹果给了b,b可以直接自己用手(this指向window)拿着吃,而不需要小贩用手(this指向obj)拿着喂你吃。

举个例子(回调引起)

function a(){    console.log(this.num);}var obj = {    num:1,    a:a}function f(fn){    fn();}var num = 2;f(obj.a); // 2f(obj.a()); // 1 TypeError , 注意此处并不是隐式丢失,而是隐式绑定 + 语法错误(1不是函数)

回调丢失this绑定对象的问题与赋值丢失类似,因为函数在进行传参的时候,其实也是赋值(fn = obj.a)。

JS内置的函数与自己定义的函数都会发生丢失问题,如setTimeout函数。

function a(){    console.log(this.num);}var obj = {    num:1,    a:a}function f(fn){    fn();}var num = 2;setTimeout(f(obj.a),1000) // 2

总之:回调和赋值两种引发隐式丢失的问题是比较常见的,这是很不友好的,所以需要解决。后面会通过固定this来解决这个问题。

(3)显式绑定

显示绑定就是使用JS提供的方法call()、apply()、bind()将this手动绑定到某个对象。

举个例子

function a(){    console.log(this.num);}obj = {    num:1}var num = 2;a(); // 2a.call(obj); // 1a.apply(obj); // 1a.bind(obj)(); // 1

解决隐式丢失(硬绑定)绑定就是在调用函数时手动改变this。

还是之前隐式丢失的例子

function a(){    console.log(this.num);}var obj = {    num:1,    a:a}function f(fn){    // fn(); // 原来的写法    // 硬绑定    fn.call(obj)}var num = 2;f(obj.a); // 1

(4)new 绑定

首先厘清一个误解:JS的 new 与其他类语言相同。

其实JS的 new 与其他类语言不同。在传统面向类的语言中(如Java),使用 new 操作符初始化类时会调用类中的构造函数。JS中的 new 操作符使用时看起来与 Java 没有什么区别,但是机制却是不同的。构造函数对于 JS 而言,只是使用 new 操作符时被调用的函数,他们并不属于某个类,也不会去实例化一个类,他们只是被 new 操作符调用的普通函数而已。

所有函数都可以使用 new 来调用,这种函数调用成为“构造函数调用”,其实并不存在所谓的“构造函数”,只是对函数的“构造调用”而已。

使用 new 调用函数,会按照下面的步骤进行:

① 创建一个全新的对象;

② 这个新对象会被执行“原型连接”;

③ 新对象会绑定到函数调用的this;

④ 如果函数没有返回其他对象,那么 new 表达式会将这个新对象返回;

举个例子

function Foo(a) {    this.num = a;    this.that = this;    console.log(this);}var bar = new Foo(1); // bar就是默认返回的新对象console.log(Foo); console.log(bar.num); // 1,新对象绑定函数调用的thisconsole.log(bar); console.log(bar === bar.that); // 函数中的this,就是新对象bar

 3.优先级

new > 显示 > 隐式 > 默认

举个例子(显示 > 隐式)

function a(){    console.log(this.num);}var obj = {    a:a,    num:1}var num = 2;obj.a(); // 1obj.a.call(window); // 2 显示绑定比隐式绑定优先

举个例子(new > 隐式)

function a(n){    this.num = n;}var obj = {    a:a,}var num = 2;obj.a(1);console.log(obj.num); // 1var b = new obj.a(3); // new 绑定比隐式绑定优先。因为 b.num的值为3,并没有受到 obj的影响console.log(obj.num);// 1console.log(b.num); // 3

举个例子(new > 显式)

function a(n){    this.num = n;}var obj = {    a:a,}var num = 2;var bar = a.bind(obj);bar(1);console.log(obj.num); // 1var b = new bar(3); // new比显示绑定优先。因为new bar(3)没有受到a.bind(obj)中obj的影响      // b.num的结果是3console.log(obj.num);// 1console.log(b.num); // 3

4.绑定例外

(1)被忽略的this

当使用显示绑定方法call()、apply()、bind(),手动修改this时,如果传入的是undefined、null或什么也不传,那么就会使用默认绑定,绑定到window(非严格模式)。

function a(){    console.log(this.num);}var num = 1;var obj = {    num:2,    a:a}a.call(obj); // 2obj.a.call(undefined); // 1 因为显示绑定优先于隐式绑定,  // 但是显示绑定又指定this为undefined,多以使用默认绑定(window)a.call(null); // 1 a.call(); // 1 a.apply(null); // 1a.bind(undefined)(); // 1

(2)更安全的this(也能够解决隐式丢失问题)

当总是使用null或undefined来忽略this可能会产生一些问题,比如一些第三方库中函数使用的this,那么就会默认绑定到window(非严格),这将会导致错误。

所以更安全的做法就是创建一个特殊的空对象,将所有函数的this限制在这个空对象中,不会对全局对象产生任何影响。使用 Object.create(null) 来创建一个比 {} 还空的对象,Object.create(null) 所创建出来的对象不会Object.prototype这个委托。

举个例子(使用null的弊端)

function foo(argu) {    console.log(this.num,argu);}var num = 1;var obj = {    num:2,    foo:foo}foo.call(null,'null'); // 1 'null' , this是windowsetTimeout(obj.foo('setTimeout'),100); // 2 'setTimeout' , 注意this是obj,因为直接执行了setTimeout(obj.foo,100); // 1 undefined , this是window

举个例子(使用 Object.create(null) )

function foo(argu) {    console.log(this.num,argu);}var num = 1;var obj = {    num:2,    foo:foo}var ø = Object.create(null);foo.call(ø,'ø'); // undefined 'ø' , this是 {}setTimeout(obj.foo.bind(ø,'ø'),100); // undefined 'ø' , this是 {}setTimeout(obj.foo.call(ø,'ø'),100); // undefined 'ø' , this是 {}

(3)软绑定

当使用硬绑定的时候,会大大降低函数的灵活性,因为使用了硬绑定(使用显示绑定固定this),则会导致隐式绑定和其他显示绑定失效。

硬绑定是防止隐式丢失问题(赋值和传参时,丢失绑定的this,将this绑定到错误的window或undefined身上)。换个解决隐式丢失问题的方式:对某个函数进行调用时,先检查它的this,如果是window或undefined,则修改它的this(目标对象this),否则不做变化。这也就是软绑定的思路。

下面的代码是《你不知道的JavaScript》这本书上的代码

// 对softBind函数进行封装if (!Function.prototype.softBind) {    Function.prototype.softBind = function (obj) { var fn = this; // 捕获所有 curried 参数 var curried = [].slice.call(arguments, 1); // 这行代码相当于arguments.slice(1),因为arguments是伪数组不是真正的数组,不能使用数组的slice方法,所以使用call(),将this绑定到数组,确保arguments可以使用数组的方法slice var bound = function () {     return fn.apply(  (!this || this === (window || global)) ? obj : this  ,curried.concat.apply(curried, arguments) // 这行代码是将bound函数参数与softBind函数参数合并  );     };     bound.prototype = Object.create(fn.prototype); return bound;    };}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,证明解决隐式丢失问题,this不是window而是objfooOBJ.call(obj3); // name: obj3,证明解决硬绑定问题,可以更改this指向obj3obj2.foo = foo.softBind(obj);obj2.foo(); // name: obj2,bound函数this指向obj2setTimeout(obj2.foo, 10);// name: obj,函数作为参数传递,this不是window,是初始化softBind函数时的obj

(4)箭头函数

箭头函数是ES新增的,箭头函数不使用上面四种绑定规则,它的this是根据外层(函数或全局)作用域来决定的。根据当前词法作用域来决定this。

举个例子

var obj1 = {    num: 2,    obj2: { num: 3, obj3: {     num:4,     a: () => {  console.log(this.num);     } }    }}var num = 1;obj1.obj2.obj3.a(); // 1// window依次传递,最终obj3的this是window,则箭头函数的this也是window

箭头函数的this一旦确定就不能被修改(new也不行),它只是由上层作用域的this决定。

举个例子

function foo() {   // 返回一个箭头函数   return ()=>{//this 继承自 foo()console.log(this.a);   };}var obj1 = {    a: 2};var obj2 = {    a: 3};var a = 4;var bar = foo.call(obj1);bar.call(obj2); // 2,obj2无法覆盖箭头函数的this(obj1)bar(); // 2,window也无法覆盖箭头函数的this(obj1)

还是上面的代码,只是把箭头函数换为普通函数

function foo() {   // 返回一个箭头函数   return function(){//this 继承自 foo()console.log(this.a);   };}var obj1 = {    a: 2};var obj2 = {    a: 3};var a = 4;var bar = foo.call(obj1);bar.call(obj2); // 3,obj2覆盖普通函数的this(obj1)bar(); // 4,window覆盖普通函数的this

使用ES6之前的 that = this,就可以实现箭头函数的功能

function foo() {   var that = this;   // 返回一个箭头函数   return function(){//this 继承自 foo()console.log(that.a);   };}var obj1 = {    a: 2};var obj2 = {    a: 3};var a = 4;var bar = foo.call(obj1); // 固定普通函数的this(obj1)bar.call(obj2); // 2,普通函数的this已经被固定bar(); // 2,