4

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、作用域、闭包、对象

详解箭头函数和普通函数的区别以及箭头函数的注意事项、不适用场景


人丑就要多读书_
197 声望17 粉丝