《你不知道的JavaScript-上卷》第二部分-this和对象原型-笔记-2-this全面解析

47 阅读9分钟

2.1 调用位置

调用位置

调用位置就是函数在代码中被调用的位置(而不是声明的位置)。

  • 调用位置就在调用栈中,当前正在执行的函数的前一个调用中
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 的调用位置

2.2 绑定规则

this 的绑定对象的四条规则

2.2.1 默认绑定

独立函数调用,最常用的,是无法应用其他规则时的默认规则

function foo() {
    console.log( this.a );
}
var a = 2;
foo(); // 2
  • 当调用 foo() 时,this.a 被解析成了全局变量 a。
  • 函数调用时应用了 this 的默认绑定,因此 this 指向全局对象

如果是严格模式,全局对象无法使用默认绑定,this就是undefined

function foo() {
  "use strict";
  console.log(this.a);
}
var a = 2;
foo(); // TypeError

一个细节是虽然 this 的绑定规则完全取决于调用位置,但是只有 foo() 运行在非 strict mode 下时,默认绑定才能绑定到全局对象;严格模式下与 foo()的调用位置无关,代码如下

function foo() {
  console.log(this.a);//此处不是严格模式,可以绑定到全局对象
}
var a = 2;
(function () {
  "use strict"; //此处是严格模式
  foo(); // 2
})();

所以,不应该混合使用 strict mode 和 non-strict mode

2.2.2 隐式绑定

调用位置是否有上下文对象

  • 当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。
  • 须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把 this 间接(隐式)绑定到这个对象上。
function foo() {
  console.log(this.a);
}
var obj = {
  a: 2,
  foo: foo 
};
obj.foo(); // 2

调用位置会使用 obj 上下文来引用函数,当 foo() 被调用时,它的落脚点确实指向 obj 对象。因为调用 foo() 时 this 被绑定到 obj,因此 this.a 和 obj.a 是一样的


对象属性引用链中只有最顶层或者说最后一层会影响调用位置

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

隐式丢失

是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上,取决于是否是严格模式。

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

虽然 bar 是 obj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身,因此此时的bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。

回调函数的形参传递也会导致隐式丢失问题

  • 参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值
    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"

即使传入内置的函数setTimeout,setInterval 等,也是同样的问题

    function foo() {
      console.log(this.a);
    }
    var obj = {
      a: 2,
      foo: foo
    };

    var a = "oops, global"; // a 是全局对象的属性
    setTimeout(obj.foo, 100); // "oops, global"

2.2.3 显式绑定

不想在对象内部包含函数引用,而想在某个对象上强制调用函数,可以使用函数的 call(..) 和apply(..) 方法,直接指定 this 的绑定对象,因此称之为显式绑定

  • 它们的第一个参数是一个对象,它们会把这个对象绑定到this,接着在调用函数时指定这个 this
  • 如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对象,这个原始值会被转换成它的对象形式(也就是 new String(..)、new Boolean(..) 或者new Number(..))。这通常被称为“装箱”。
    function foo() {
      console.log(this.a);
    }
    var obj = {
      a: 2
    };
    foo.call(obj); // 2

通过 foo.call(..),我们可以在调用 foo 时强制把它的 this 绑定到 obj 上。


显式绑定的变种

1 硬绑定

    var a = 100;
    function foo() {
      console.log(this.a);
    }
    var obj = {
      a: 2
    };
    var bar = function () {
      foo.call(obj); //强制把 foo 的 this 绑定到了 obj,之后如何调用函数 bar,它总会手动在 obj 上调用 foo
    };
    bar(); // 2
    setTimeout(bar, 100); // 2
    // 硬绑定的 bar 不可能再修改它的 this
    bar.call(window); // 2

典型应用场景,包裹函数,传入所有的参数并返回接收到的所有值

    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

  • bind(..) 会返回一个硬编码的新函数,它会把参数设置为 this 的上下文并调用原始函数
    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

2 API调用的“上下文”

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

  • 这些函数实际上就是通过 call(..) 或者 apply(..) 实现了显式绑定
    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

2.2.4 new绑定

JavaScript 中的“构造函数”

JavaScript 中 new 的机制实际上和面向类的语言完全不同,构造函数只是一些 使用 new 操作符时被调用的函数,它们并不会属于某个类,也不会实例化一个类, 它们只是被 new 操作符调用的普通函数而已

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

构造调用或者说使用 new 来调用函数时,会自动执行下面的操作

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行 [[ 原型 ]] 连接。
  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 绑定

2.3 优先级

this 绑定的四条规则怎么判断,需要优先级判断

  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()

2.4 绑定例外

2.4.1 被忽略的this

如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定规则

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

应用场景

  • 是使用 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 绑定到这个对象不会对你的程序产生任何副作用。

  • 创建一个空对象最简单的方法都是 Object.create(null)
  • Object.create(null) 和 {} 很 像, 但 是 并 不 会 创 建 Object.prototype 这个委托,所以它比 {}“更空”:
function foo(a,b) {
    console.log( "a:" + a + ", b:" + b );
}

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

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

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

2.4.2 间接引用

有可能(有意或者无意地)创建一个函数的“间接引用”,在这种情况下,调用这个函数会应用默认绑定规则

间接引用最容易在赋值时发生

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

2.4.3 软绑定

softBind

如果可以给默认绑定指定一个全局对象和 undefined 以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改 this 的能力。

  • 对指定的函数进行封装,首先检查调用时的 this,如果 this 绑定到全局对象或者 undefined,那就把指定的默认对象 obj 绑定到 this,否则不会修改 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;
  };
}


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 <---- 应用了软绑定

2.5 this词法

箭头函数不使用 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 绑定到 obj1,bar(引用箭头函数)的 this 也会绑定到 obj1,箭头函数的绑定无法被修改。(new 也不行!)

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

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

}

var obj = {
    a:2
};

foo.call( obj ); // 2