call-stack & call-site

调用栈:函数调用的顺序。

调用点:调用函数时所在的调用栈。

结合下面这个示例,可以更好的理解这两个概念。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function foo1 () {
// call-stack: global->foo1
console.log('foo1');

// call-site: foo2
foo2();
}

function foo2 () {
// call-stack: global->foo1->foo2
console.log('foo2');

// call-site: foo3
foo3();
}

function foo3 () {
// call-stack: global->foo1->fool2->fool3
console.log('foo3');
}

foo1(); // call-stack: global, call-site: foo1

this的指向

this的指向是由函数运行是的调用点决定的。并且是遵循下面四个规则:

  1. 默认绑定

    默认绑定会将this只想全局作用域,如果是在严格模式下,则不会只想全局作用域。同时,默认绑定是四个绑定规则中,优先级最低的。即,如果有其他形式的绑定规则出现,就不会使用默认绑定了。

    下面是示例:

    1
    2
    3
    4
    5
    6
    7
    8
    // 全局作用域
    function foo () {
    console.log(this.a);
    }

    var a = 1;

    foo(); // 1

    如果是严格模式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 全局作用域
    'use strict';

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

    var a = 1;

    try {
    foo();
    } catch (err) {
    console.log(err); // TypeError: Cannot read property 'a' of undefined
    }
  2. 隐式绑定

    隐式绑定是指一个对象调用它内部的函数时,被调用的函数内部的this指向这个对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function foo () {
    console.log(this.a);
    }

    let obj = {
    a: 1,
    foo: foo
    }

    obj.foo(); // 隐式绑定,this指向obj,所以打印的是 1

    链式隐式绑定时,this指向的是最后一个调用函数的对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function foo () {
    console.log(this.a);
    }

    let obj1 = {
    a: 1,
    foo: foo
    }

    let obj2 = {
    a: 2,
    obj1: obj1
    }

    obj2.obj1.foo(); // this指向最后一个调用函数的对象obj1,打印的结果是 1

    隐式绑定丢失:

    隐式绑定必须用obj.func的形式调用,如果期间发生引用赋值,被赋值的对象会发生隐式绑定丢失,因为被赋值的对象其实只是拿到了函数的引用,最后调用的时候使用的默认绑定的规则。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    let obj = {
    a: 'obj',
    foo: function () { console.log(this.a); }
    }

    var a = 'global';
    let bar = obj.foo; // 隐式绑定丢失

    function baz(fn) {
    fn(); // 默认绑定
    }

    obj.foo(); // 隐式绑定,打印结果:obj
    bar(); // 默认绑定,打印结果:global
    baz(obj.foo); // 隐式绑定丢失,打印结果:global

    如果foo是箭头函数咋整?箭头函数没有自己的this指针,而是与外层的this绑定在一起的,所以这个时候就需要确定外层的this的绑定。比如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function foo1 () {
    console.log(this.a);
    }

    const foo2 = () => {
    console.log(this.a); // this指向foo2所在的作用域,没有a的变量声明,打印结果是undefined
    };

    const obj = {
    a: 1,
    foo1,
    foo2
    };

    obj.foo1(); // 隐式绑定,this指向obj,所以打印的是 1
    obj.foo2(); // undefined

    更多细节在:YDKJSY 3-2, P26

  3. 显示绑定

    显示绑定是通过apply、call、bind来实现的,可以指定this需要指向的对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function foo () {
    console.log(this.a);
    }

    const obj1 = {
    a: 1
    }
    const obj2 = {
    a: 2
    }

    foo.call(obj1); // 1,将foo中的this指向obj1
    foo.apply(obj2); // 2,将foo中的this指向obj2

    显然每次使用foo的时候都需要调用一次,如果this指向的对象是可变的,倒也还好。如果this指向的是固定不变的,这个时候apply和call就不方便了。所以,这个时候需要用bind。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function foo () {
    console.log(this.a);
    }

    const obj1 = {
    a: 1
    }

    const obj2 = {
    a: 2
    }

    const bar = foo.bind(obj1);
    bar(); // 1,这个时候不会发生类似隐式绑定的this丢失,this始终指向obj
    bar.call(obj2); // 2,即使再用apply、bind也不会改变this的指向了

    具体可以看一下bind是如何用aplly、call实现的。

  4. new 绑定

    new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。new 关键字会进行如下的操作:

    • 创建一个空对象
    • 将这个的对象的__ proto __指向构造函数的prototype
    • 将构造函数的this指向新对象
    • 如果构造函数有返回值,就返回值,没有这个新对象。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function foo (a) {
    this.a = a;
    }

    foo.prototype.name = 'foo';
    foo.prototype.sayName = function () {
    console.log(this.name);
    };

    const bar = new foo(2); // bar.__proto__ = foo.prototype
    console.log(bar.a); // 2
    console.log(bar.name); // foo
    bar.sayName(); // foo

绑定的优先级

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

如果显示绑定使用的传入的this指向是null或者undefined的话,就会退化成默认绑定。

1
2
3
4
5
6
function foo () {
console.log(this.a);
}

const a = 2;
foo.call(null); // 默认绑定,相当于foo(),打印结果是:2

apply实现bind

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - what ' +
'is trying to be bound is not callable'
);
}

var aArgs = Array.prototype.slice.call(arguments, 1);
var fToBind = this;
var fNOP = function () {};
var fBound = function () {
return fToBind.apply(
(
this instanceof fNOP &&
oThis ? this : oThis
),
aArgs.concat(Array.prototype.slice.call(arguments))
);
}
;

fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}

由此可见,bind是返回了一个始终func.apply(args)的函数,所以每次调用的时候,才不用再次显示调用apply了。