songStar0904 / You-don-t-know-JavaScript

The notes of 你不知道的JavaScript(上)

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

第二部分 this和对象原型 第二章----this全面解析

songStar0904 opened this issue · comments

2.1 调用位置

在理解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调用位置

2.2 绑定规则

2.2.1 默认绑定

说白了, 默认绑定就是this 指向全局对象。

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(); // this is undefined

注意: 虽然this 的绑定规则完全取决于调用位置, 但是只有foo() 运行在非strict mode 下时, 默认绑定才会绑定到全局对象。 严格模式下与foo() 的调用位置无关。

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

2.2.2 隐式绑定

隐式绑定考虑的规则是调用位置是否有上下文对象, 或者说是否被某个对象拥有或者包含。

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

当foo() 被调用时, 它的落脚点指向obj 对象。 当函数引用有上下文对象时, 隐式绑定规则会把函数调用中的this 绑定到这个上下文对象。 因为调用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 绑定问题就是被隐式绑定的函数会丢失绑定对象, 也就是说它会应用默认绑定。

function foo(){
  console.log(this.a);
}
var obj = {
  a:2,
  foo: foo
}
var bar = obj.foo; // 函数别名
var a = 'global';
bar(); // 'global

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

function foo(){
  console.log(this.a);
}
function doFoo(fn){
  // fn 其实引用的是ifoo
  fn(); // 调用位置
}
var obj = {
  a: 2,
  foo: foo
}
var a = 'global';
doFoo(obj.foo); // 'global'

参数传递其实就是一中隐式赋值, 因此我们传入函数时也会被隐式赋值, 结果就同上。
如果把函数传入语言内置的函数而不是自己声明的函数, 结果一样。

// 声明同上
...
setTimeout(obj.foo, 100); // 'global'

2.2.3 显示绑定

call 和 apply 区别:

  • 参数, 第一个参数都是所执行的环境(也就是this), 第二个参数是执行函数的参数, call是单个单个传入, apply是以数组的形式传入。
function foo(){
  console.log(this.a);
}
var obj = {
  a: 2
}
foo.call(obj); // 2

通过foo.call(), 我们可以在调用foo 时强制把它的this 绑定到obj 上。
如果你传入了一个原始值(字符串, 布尔, 数字类型)来当作this 的绑定对象, 这个原始值会被转换为它的对象形式(new String(), new Boolean() new Number())。 这被称为‘装箱’。

硬绑定
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。
应用场景: 创建一个包裹函数, 传入所有的参数并返回接受到所有值:

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

另一种使用方法是创建一个i 可以重复使用的辅助函数:

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
API 调用的上下文
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 绑定

在传统的面向对象的语言中, ‘构造函数’ 是类中的一些特殊的方法, 使用new 初始化类时会调用类中的构造函数。 通常是这样的:

something = new MyClass(...);

JavaScript 中new 的机制和面向类语言完全不同。 在JavaScript 中构造函数只是一些使用new 操作符时被调用的函数。 它们并不属于某个类, 也不会实例化一个类。 它们只是被new操作符调用的普通函数。
包括内置对象在内的所有函数都可以用new 来调用, 这种函数调用被称为构造函数的调用。 区别: 实际上并不存在所谓的‘构造函数’, 只有对于函数的’构造调用‘

使用new 来调用函数, 会自动执行下面操作:

  • 创建一个新的对象。
  • 这个对象会被执行[[原型]]连接。(将新创建的空对象的隐式原型指向其构造函数的显示原型。)
  • 这个新对象会绑定到函数调用的this。 (将this 指向这个新对象)
  • 如果函数没有返回其他对象, 那么new 表达式中的函数调用会自动返回这个新对象。如果有返回值, 就返回这个返回值。

2.3 优先级

显示绑定 > new 绑定 > 隐式绑定 > 默认绑定

function foo(something){
  this.a = something;
}
var obj = {};
var bar = foo.bind(obj);
bar(2);
console.log(obj.a); // 2
var baz = new bar(3);
console.log(baz.a); // 3
console.log(obj.a); // 2

bar 被绑定到obj 上, 但是 new bar(3) 并没有向我们语气的那样把obj.a 修改为3. 相反 new 修改了硬绑定调用bar(...) 中的this。 因为使用了new 绑定, 我们的到baz 的新对象, baz,a的值是3。

if (!Function.prototype.bind) {
  Function.prototype.bind = function(oThis) {
    if (typeof this !== 'function') {
      // 与ES5最接近的
      // 内部IsCallable 函数
      throw new TypeError(
        'Function.prototype.bind - what is tring to be bound is not callable'
      );
    }
    var aArgs = Array.prototype.slice.call(arguments, 1),
        fToBind = this,
        fNOP = function(){},
        fBound = function() {
          return fToBind.apply((this instanceof fNOF && oThis ? this : oThis),
          aArgs.concat(Array.prototype.slice.call(arguments)));
        };
        fNOP.prototype = this.prototype;
        fBound.prototype = new fNOP();
        return fBound;
  }
}

下面是new 修改 this 的相关代码:

this instanceof fNOP && oThis ? this : oThis
// ... 以及
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();

这段代码会判断硬绑定函数是否是被new 调用, 如果是的话就会使用新创建的this 代替硬绑定的 this。
之所以要在new 中使用硬绑定函数, 主要目的是预先设置函数的一些参数, 这样在使用new 进行初始化时就可以至传入其余参数。(柯里化)。

function foo(p1, p2){
  this.val = p1 + p2;
}
// 之所以使用null 是因为在本例中我们并不关心硬绑定的this 是什么
// 反正使用new 时 this 会被修改
var bar  = foo.bind(null, 'p1');
var baz = new bar('p2');
baz.val; // p1p2

判断this

  1. 函数是否在new 中调用(new 绑定)? 如果是的话this 绑定的是新创建的对象。
    var bar = new foo();

  2. 函数是否通过call, apply(显示绑定)或者硬绑定调用? 如果是, this 绑定的是指定的对象。
    var bar = foo.call(obj);

  3. 函数是否某个上下文中调用 (隐式调用)? 如果是, this绑定的是那个上下文对象。
    var bar = obj.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

常见的传入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

在ES6中可以用...操作符代替apply(...) 来展开数组, foo(...[2,3]) === foo(2,3) , 这样可以避免不必要的this绑定。
无脑使用null 来忽略this绑定会产生一些副作用。 特别是在一些第三方库中, 把this 绑定到全局对象, 会导致不可预计的后果。
更安全的this:

function foo(a, b){
  console.log('a: ' + a + ', b: ' + b);
}
var ø = Object.create(null);
foo.apply(ø, [2, 3]); // a:2, b: 3
var bar = foo.bind(ø, 2);
bar(3); // a:2, b:2

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

赋值表达式 p.foo = o.foo 的返回值是目标函数的引用, 因此调用位置是foo() 而不是 p.foo() 或者 o.foo()。 注意: 对于默认绑定来说, 决定this绑定对象的并不是调用位置是否处于严格模式, 而是函数体是否处于严格模式。 如果函数体处于严格模式, this 会被绑定到undefined, 否则this 会绑定到全局对象。