关于javascript:JavaScript总结this绑定全面解析

5次阅读

共计 11266 个字符,预计需要花费 29 分钟才能阅读完成。

this 是什么

当一个函数被调用时,会创立一个 执行上下文。这个执行上下文会蕴含函数在哪里被调用(执行栈)、函数的调用形式、传入的参数等信息。this 就是这个执行上下文的一个属性,会在函数执行的过程中用到。

调用地位

在了解 this 的绑定过程之前,首先要了解调用地位:调用地位就是函数在代码中 被调用的地位(而不是申明的地位)。

咱们关怀的调用地位就在以后正在执行的函数的 前一个调用 中。

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 的绑定规定总共有 5 种:

  1. 默认绑定
  2. 隐式绑定
  3. 显示绑定
  4. new 绑定
  5. 箭头函数绑定

1. 默认绑定

独立函数调用:能够把这条规定看作是无奈利用其余规定时的默认规定。

function foo() {console.log( this.a);
}

var a = 2;

foo(); // 2

严格模式(strict mode),不能将全局对象用于默认绑定,this 会绑定到undefined

function foo() { 
    "use strict";
    
    console.log(this.a);
}

var a = 2;

foo(); // TypeError: Cannot read property 'a' of undefined

2. 隐式绑定

当函数援用有 上下文对象 时,隐式绑定 规定会把函数中的 this 绑定到这个上下文对象。

function foo() {console.log( this.a);
}

var obj = {
    a: 2,
    foo: foo
};

obj.foo(); // 2

对象属性援用链中只有上一层或者说最初一层在调用地位中起作用。

function foo() {console.log( this.a);
}

var obj2 = {
    a: 42,
    foo: foo
};
var obj1={
    a:2,
    obj2:obj2
}
obj1.obj2.foo() // 42

隐式失落

隐式绑定 的函数特定状况下会失落绑定对象,它会利用 默认绑定 ,把this 绑定到全局对象或者 undefined 上(取决于是否是严格模式)。

// 尽管 bar 是 obj.foo 的一个援用,然而实际上,它援用的是 foo 函数自身。// 因而此时的 bar()是一个不带任何润饰的函数调用,因而利用了默认绑定。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"

参数传递其实就是一种 隐式赋值,咱们传入函数时也会被隐式赋值。

如果把函数传入语言内置的函数而不是传入本人申明的函数,后果是一样的。

function foo() {console.log( this.a);
}

var obj = {
    a: 2,
    foo: foo
};

var a = "oops, global"; // a 是全局对象的属性

setTimeout(obj.foo,100); // "oops, global"

// JS 环境中内置的 setTimeout()函数实现和上面的伪代码相似:function setTimeout(fn, delay) {
    // 期待 delay 毫秒
    fn(); // <-- 调用地位!}

3. 显式绑定

能够应用函数的 call(...)apply(...) 办法,它们的第一个参数是一个对象,是给 this 筹备的,接着在调用函数时将其绑定到 this。因为你能够间接指定this 的绑定对象,因而咱们称之为 显式绑定

function foo() {console.log( this.a);
}

var obj = {a: 2};

foo.call(obj); // 2

通过 foo.call(...) 咱们能够在调用 foo 时强制把它的 this 绑定到obj 上。

惋惜,显式绑定 依然无奈解决咱们之前提出的失落绑定的问题。

硬绑定

然而显式绑定的一个变种能够解决这个问题。

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), 因而强制把foothis绑定到了obj

无论之后如何调用函数 bar,它总会手动在obj 上调用 foo。咱们称之为 硬绑定

典型利用场景是创立一个包裹函数,负责接管参数并返回值:

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

另一种应用办法是创立一个能够重复使用的辅助函数:

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

ES5 提供了内置的办法Function.prototype.bind

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 的上下文并调用原始函数。

API 调用的“上下文”

第三方库以及 JavaScript 语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和 bind(...) 一样,确保你的回调函数应用指定的this

function foo(el) {console.log( el, this.id);
}

var obj = {id: "awesome"}

let myArr = [1,2,3]
// 调用 foo(..)时把 this 绑定到 obj
myArr.forEach(foo, obj);
// 1 awesome 2 awesome 3 awesome

这些函数实际上就是通过 call(...) 或者 apply(...) 实现了 显式绑定

4.new 绑定

在 Javascript 中,构造函数 只是一些应用 new 操作符时被调用的函数。它们并不属于某个类,也不会实例化一个类。

包含内置对象函数 (比方Number(...)) 在内的所有函数都能够用 new 来调用,这种函数调用被称为结构函数调用。

实际上并不存在所谓的“构造函数”,只有对于函数的“结构调用”。

应用 new 来调用函数,或者说产生结构函数调用时,会主动执行上面的操作。

  1. 创立(或者说结构)一个全新的对象。
  2. 这个新对象会被执行 [[Prototype]] 连贯。
  3. 这个新对象会绑定到函数调用的this
  4. 如果函数没有返回其余对象,那么 new 表达式中的函数调用会主动返回这个新对象。
function foo(a) {this.a = a;}

var bar = new foo(2);
console.log(bar.a); // 2

应用 new 来调用 foo(...) 时,咱们会结构一个新对象并把它绑定到 foo(...) 调用中的 this 上。

new是又一种能够影响函数调用时 this 绑定行为的办法,咱们称之为 new 绑定。

优先级

function foo1() {console.log(this.a)
}

function foo2(something) {this.a = something}

var obj1 = {
    a: 2,
    foo: foo1
}

var obj2 = {
    a: 3,
    foo: foo1
}
var obj3 = {foo: foo2}

var obj4 = {}

obj1.foo(); //2
obj2.foo(); //3

obj1.foo.call(obj2); //3
obj2.foo.call(obj1); //2
// 可见,显式绑定比隐式绑定优先级高

obj3.foo(4);
console.log(obj3.a); //4

obj3.foo.call(obj4, 5);
console.log(obj4.a); //5

var bar = new obj3.foo(6);
console.log(obj3.a); //4
console.log(bar.a); //6
// 可见,new 绑定比隐式绑定优先级高

var qux = foo2.bind(obj4);
qux(7);
console.log(obj4.a); //7

var quux = new qux(8);
console.log(obj4.a); //7
console.log(quux.a); //8
//new 绑定批改了硬绑定(到 obj4 的)调用 qux(...)中的 this。

当初,咱们能够依据优先级来判断函数在某个调用地位利用的是哪条规定。

  1. 函数是否在 new 中调用(new绑定),如果是的话 this 绑定的是新创建的对象。
  2. 函数是否通过 callapply(显式绑定)或者硬绑定调用,如果是的话this 绑定的是指定的对象。
  3. 函数是否在某个上下文对象中调用(隐式绑定),如果是的话 this 绑定的就是那个上下文对象。
  4. 如果都不是的话,应用默认绑定。

    • 如果在严格模式下,就绑定到undefined
    • 否则就绑定到全局对象。

绑定例外

被疏忽的 this

如果你把 null 或者 undefined 作为 this 的绑定对象传入 callapply 或者bind,这些值在调用时会被疏忽,理论利用的是默认规定。

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

两种状况会传入null

  • 应用 apply(...) 来“开展”一个数组,并当做参数传入一个函数。
  • bind(...)能够对参数进行柯里化(事后设置一些参数)。
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 绑定到全局对象,这将导致不可预计的结果(比方批改全局对象)。

更平安的 this

一种“更平安”得做法是传入一个非凡的对象,把 this 绑定到这个对象不会对你的程序产生任何副作用。

在 Javascript 中创立一个空对象最简略的办法是 Object.create(null)。它和{} 很想,然而并不会创立 Object.prototype 这个委托。

function foo(a, b) {console.log( "a:" + a + ",b:" + b);
}

// 咱们的空对象
var ø = Object.create(null);

// 把数组”开展“成参数
foo.apply(ø, [2, 3] ); // a:2,b:3

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

间接援用

另一个须要留神的是,你有可能创立一个函数的“间接援用”,调用这个函数会利用 默认绑定 规定。

间接援用 最容易在赋值时产生:

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 强制绑定到指定的对象(除了应用 new 时),避免函数调用利用默认绑定规定。

毛病是硬绑定会大大降低函数的灵活性,应用 硬绑定 之后就无奈应用 隐式绑定 或者 显式绑定 来批改this

如果能够给 默认绑定指定一个全局对象和 undefined 以外的值,那就能够实现和硬绑定雷同的成果,同时保留隐式绑定或者显式绑定来批改this

if(!Function.prototype.softBind) {Function.prototype.softBind = function(obj) {
        var fn = this;
        // 捕捉所有 curried 参数
        var curried = [].slice.call( arguments, 1); 
        var bound = function() {
            return fn.apply((!this || this === (window || global)) ? 
                    obj : this,
                curried.concat.apply(curried, arguments)
            );
        };
        bound.prototype = Object.create(fn.prototype);
        return bound;
    };
}

除了软绑定之外,softBind(...)的其它原理和 ES5 内置的 bind(...) 相似。

它会对指定的函数进行封装,首先查看调用时的 this,如果this 绑定到全局对象或者 undefined,那就把指定的默认对象obj 绑定到this,否则不会批改this

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

能够看到,软绑定版本的 foo() 能够手动将 this 绑定到 obj2 或者 obj3 上,但如果利用默认绑定,则会将 this 绑定到obj

箭头函数

咱们之前介绍的四条规定能够蕴含所有失常的函数。然而 ES6 中介绍了一种无奈应用这些规定的非凡函数类型:箭头函数。

箭头函数不应用 this 的四种规范规定,而是依据外层(函数或者全局)作用域来决定this

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, 不是 3!

foo()外部创立的箭头函数会捕捉调用时的 foo()this。因为 foo()this绑定到 obj1bar(援用箭头函数)的this 也会绑定到 obj1,箭头函数的绑定无奈被修该。(new 也不行!)

箭头函数罕用于回调函数中,例如事件处理器或者定时器:

function foo(){setTimeout(()=>{// 这里的 this 在词法上继承自 foo()
        console.log(this.a);
    },100);
}

var obj = {a:2};

foo.call(obj) //2

箭头函数中的 this

1. 箭头函数没有prototype(原型),所以箭头函数自身没有this

let a = () => {}
console.log(a.prototype) //undefined

2. 箭头函数中的 this 是从定义它们的上下文继承的(Javascript 权威指南第 7 版 P206),继承自外层第一个一般函数的this

let foo
let barObj = {msg: 'bar 的 this 指向'}
let bazObj = {msg: 'baz 的 this 指向'}

bar.call(barObj) //bar 的 this 指向 barObj
baz.call(bazObj) //baz 的 this 指向 bazObj

function bar() {foo = () => {console.log(this, 'this 指向定义它们的上下文,外层的第一个一般函数')
  }
}

function baz() {foo() 
}

//msg: "bar 的 this 指向" "this 指向定义它们的上下文,外层的第一个一般函数"

3. 箭头函数的 this 无奈通过 bindcallapply间接 批改。

let quxObj = {msg: '尝试间接批改箭头函数的 this 指向'}
function baz() {foo.call(quxObj)
}

//{msg: "bar 的 this 指向"} "this 指向定义它们的上下文,外层的第一个一般函数"

间接批改箭头函数的指向:

bar.call(bazObj) // 一般函数 bar 的 this 指向 bazObj,外部的箭头函数也会指向 bazObj

被继承的一般函数的 this 指向扭转,箭头函数的 this 指向也会跟着扭转。

4. 如果箭头函数没有外层函数,this指向 window

var obj = {
  i: 10,
  b: () => console.log(this.i, this),
  c: function() {console.log( this.i, this)
  }
}
obj.b()//undefined, window
obj.c()//10, {i: 10, b: ƒ, c: ƒ}

练习

/**
 * Question 1
 * 非严格模式下
 */

var name = 'window'

var person1 = {
  name: 'person1',
  show1: function () {console.log(this.name)
  },
  show2: () => console.log(this.name),
  show3: function () {return function () {console.log(this.name)
    }
  },
  show4: function () {return () => console.log(this.name)
  }
}
var person2 = {name: 'person2'}

person1.show1()
person1.show1.call(person2)

person1.show2()
person1.show2.call(person2)

person1.show3()()
person1.show3().call(person2)
person1.show3.call(person2)()

person1.show4()()
person1.show4().call(person2)
person1.show4.call(person2)()

正确答案如下:

person1.show1() // person1,隐式绑定
person1.show1.call(person2) // person2,显式绑定

person1.show2() // window,箭头函数绑定,没有外层函数,指向 window
person1.show2.call(person2) // window,箭头函数绑定,不能间接批改,还是指向 window

person1.show3()() // window,高阶函数,person1.show3()返回一个函数ƒ(){console.log(this.name)}到全局
                // 从而导致最终函数执行环境是 window,所以此时 this 指向 var name = 'window'
person1.show3().call(person2) // person2 , 返回函数当前,显式绑定 person2,this 指向 person2 对象
person1.show3.call(person2)() // window,高阶函数ƒ(){return function(){console.log(this.name)}} 显式绑定 person2,// 也就是高阶函数 this 指向 person2,它的返回值ƒ(){console.log(this.name)}执行环境是 window,同上           
person1.show4()() // person1,箭头函数绑定,this 是从定义函数的上下文继承的,也就是外层函数所在的上下文,外层函数的 this 指向 person1
person1.show4().call(person2) // person1,无奈通过 call 间接批改箭头函数绑定
person1.show4.call(person2)() // person2,高阶函数,外层函数 this 显式绑定 person2,批改箭头函数的外层函数 this 指向,能够扭转箭头函数 this 指向
/**
 * Question 2
 */
var name = 'window'

function Person (name) {
  this.name = name;
  this.show1 = function () {console.log(this.name)
  }
  this.show2 = () => console.log(this.name)
  this.show3 = function () {return function () {console.log(this.name)
    }
  }
  this.show4 = function () {return () => console.log(this.name)
  }
}

var personA = new Person('personA')
var personB = new Person('personB')

personA.show1()
personA.show1.call(personB)

personA.show2()
personA.show2.call(personB)

personA.show3()()
personA.show3().call(personB)
personA.show3.call(personB)()

personA.show4()()
personA.show4().call(personB)
personA.show4.call(personB)()

正确答案如下:

personA.show1() // personA,new 绑定当前,构造函数 Person 中的 this 绑定到 personA,Person 传入的参数 personA,所以后果是 personA
personA.show1.call(personB) // personB,new 绑定当前,构造函数 Person 中的 this 绑定到 personA,personA.show1 就是ƒ(){console.log(this.name)}
                // 再显式绑定 personB,personA.show1 的 this 指向 personB 实例对象,所以后果是 personB
personA.show2() // personA,new 绑定当前,构造函数 Person 中的 this 绑定到 personA,personA.show2 就是()=>console.log(this.name)
                // 而后箭头函数绑定,调用箭头函数,this 指向外层函数的 this.name,也就是 personA
personA.show2.call(personB) // personA,new 绑定当前,构造函数 Person 中的 this 绑定到 personA,personA.show2 就是()=>console.log(this.name)
                // 箭头函数不能间接批改,所以还是 personA
personA.show3()() // window,new 绑定当前,构造函数 Person 中的 this 绑定到 personA,personA.show3()返回一个函数ƒ(){console.log(this.name)}到全局
          // 执行环境是 window,所以执行当前的后果是 var name = 'window',也就是 window
personA.show3().call(personB) // personB,new 绑定当前,构造函数 Person 中的 this 绑定到 personA,// personA.show3()返回一个函数ƒ(){console.log(this.name)}到全局,// 再显式绑定 personB,所以最终后果是 personB
personA.show3.call(personB)() // window,new 绑定当前,构造函数 Person 中的 this 绑定到 personA,// 高阶函数ƒ(){return function(){console.log(this.name)}}显式绑定 personB,// 返回一个函数ƒ(){console.log(this.name)}到全局
                 // 执行环境是 window,所以执行当前的后果是 var name = 'window',也就是 window
personA.show4()() // personA,new 绑定当前,构造函数 Person 中的 this 绑定到 personA,// 高阶函数ƒ(){return ()=>console.log(this.name)}执行后返回箭头函数()=>console.log(this.name),执行箭头函数
          // 箭头函数绑定,继承外层一般函数 this,所以后果是 personA
personA.show4().call(personB) // personA,new 绑定当前,构造函数 Person 中的 this 绑定到 personA,// 高阶函数ƒ(){return ()=>console.log(this.name)}执行后返回箭头函数()=>console.log(this.name),// 箭头函数不能间接批改,所以后果还是 personA
personA.show4.call(personB)() // personB,new 绑定当前,构造函数 Person 中的 this 绑定到 personA,// 显式绑定 外层函数,所以箭头函数也被批改为 personB

参考

[你不晓得的 JavaScript 上卷]

[Javascript 权威指南第七版]

从这两套题,重新认识 JS 的 this、作用域、闭包、对象

详解箭头函数和一般函数的区别以及箭头函数的注意事项、不实用场景

正文完
 0