参考 《你不知道的JavaScript上卷》

我们经常会对this的指向存在疑惑,搞不清楚是指向window,还是本身。

this 的解析

  • this是在运行时进行绑定的,并不是在编写时绑定,
  • 它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式

this 规则

我们要清楚知道在函数的执行过程中调用位置是如何确定this的绑定对象的

我们分析 函数是属于以下四种规则的哪一种既可。

  • 默认绑定
  • 隐式绑定
  • 显示绑定
  • 硬绑定

默认绑定

默认绑定是最常用的一种方式,可以把这条规则看作是无法应用其他规则时的默认规则。

// 例子

function foo() {
    console.log(this); // this 指向的是window
}

// 此时我们调用的位置在全局,那么foo 的this指向的是window
foo();

注意:默认绑定在严格模式下,会报错。 严格模式下,默认绑定会将this绑定到undefined

隐式绑定

一般来说隐式绑定就是obj.func()这样子。但我们看看下面的注意事项

// 这里要注意foo的声明方式
function foo() {
    console.log(this.a) // 123
}

const obj = {
    a: "123",
    foo: foo
}
//obj对象引用了foo, 调用位置会使用obj上下文来引用函数,因此你可以说函数被调用时obj对象“拥有”或者“包含”它。
obj.foo();

隐式调用: 当函数引用有上下文的时候,函数的this会绑定到上下文对象中。

但是对象属性引用链中只有上一层或者说最后一层在调用位置中起作用

// 例子
function foo() {
    console.log(this.a) 
}

const obj = {
    a: "123",
    foo: foo
}

const obj2 = {
    a: "2",   // 
    obj: obj
}

// 这里是最后一层起作用了
obj2.obj.foo();  // 123

隐式丢失

简单说就是函数丢失了原来的绑定对象,然后函数使用默认绑定的方式,导致了this指向window或者是undefined


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

const obj = {
    a: "123",
    foo: foo
}
var a = "windows";

// 虽然 obj.foo 引用的是foo 函数,但是它等同于使用foo(), 这样看的话他的调用方式是全局的,所以使用默认绑定
const test = obj.foo;
test()  // windows,  

另外还有回调函数会造成this的丢失,所以我们一般用call或者apply去改变this的指向

// 例子
function foo() {
    console.log(this.a) 
}

const obj = {
    a: "123",
    foo: foo
}
var a = "windows";

function doFoo(fn) {
    // fn => foo
    fn()
}

doFoo(obj.foo)

显示绑定

显示绑定主要是使用了 callapply, 我们可以理解 当函数调用是,this 的指向更改为call函数或者apply函数的第一个参数

var obj = {
    a: "123"
}

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

foo.call(obj); // 123, 我们理解成 当foo函数调用的时候 this 指向 obj

我们可以理解为 foo.call(obj) => foo(), 但是this指向了obj

我们看下一个最常用的操作

// 辅助函数
function bind(fn, obj) {
    return function() {
        return fn.apply(obj, arguments);
    }
} 

function foo(params) {
    console.log(this.a, params)
}

var obj = {
    a: "123"
}

var test = bind(foo, obj); // bind函数返回的是function
test("hahah") // 输出: 123,hahah

new 绑定

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

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行[[Prototype]]连接。
  3. 这个新对象会绑定到函数调用的this。
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。

我们先看看new的实现源码

function new(fn, ...args) {
    let obj = {};
    obj._proto_ = fn.prototype; 
    let res = fn.call(obj, ...args);

    let isObject = typeof res === 'object' && typeof res !== null;
    let isFunction = typeof res === 'function';
    return isObject || isFunction ? res : obj;
}

看到上面第三步,call 改变了fn this的指向

function foo(a) {
    this.a = a
}

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

优先级

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

我们看看 如何实现 apply 和 call方法

call

// call 方法类似, call 方法实现方式跟下面一样
var foo = {
    value: 1,
    bar: function() {
        console.log(this.value)
    }
}
foo.bar() // 1

// fn.call(obj, 1, 2)
Function.prototype.imitateCall = function (context) {
    // 这里的context 同等于上面的 foo, 如果context没有那么我们指定到window
    context = context || window    

    // 当前this 指的是 function
    context.invokFn = this    
    // 截取作用域对象参数后面的参数
    let args = [...arguments].slice(1)
    // 执行调用函数,记录拿取返回值
    let result = context.invokFn(...args)
    // 销毁调用函数,以免作用域污染
    Reflect.deleteProperty(context, 'invokFn')
    return result
}

apply

// fn.apply(obj, [1, 2])
Function.prototype.imitateApply = function (context) {
    // 这里的context 同等于上面的 foo, 如果context没有那么我们指定到window
    context = context || window    
    // 当前this 指的是 function
    context.invokFn = this    
    // 截取作用域对象参数后面的参数
    let result
    if (arguments[1]) {
        result = context.invokFn(...arguments[1])
    } else {
        result = context.invokFn()
    }
    // 销毁调用函数,以免作用域污染
    Reflect.deleteProperty(context, 'invokFn')
    return result
}