深入基础:关于this

98 阅读6分钟

对于每个执行上下文都有三个重要的属性,

  • 变量对象
  • 作用域链
  • this。

为什么会有this

根据阮一峰的JavaScript 的 this 原理 ,this的由来与内存里面的数据结构有关系。比如:

var obj = { 
    bar: 5
    foo: function(){}
};

对于这段代码,js引擎会生成一个对象,然后把这个对象的内存地址赋值给obj。在读取的时候引擎先从obj拿到内存地址,再从这个地址读出原始的对象,返回他的bar属性。原始对象是以字典结构保存,每个属性名对应一个属性描述对象。

{
    bar:{
        [[value]]:5
        [[writable]]:true
        [[enumerable]]:true
        [[configurable]]:true
    }
}

对于属性值为一个函数的情况,引擎会把函数单独保存在内存里面,然后将函数的地址赋给value属性。

所以我们可以看到,函数实际上可以在不同的环境执行。因为函数可以在不同的运行环境执行,所以this的目的就是在函数题的内部获得当前的运行环境(context)。

这里我们总结一下this的规则:

  • this既不指向函数自身,也不指向函数的词法作用域
  • this实际上是在函数被调用的时候发生的绑定,它指向什么完全取决于函数被调用的位置

this的绑定规则

  1. 默认绑定,this指向全局对象
  2. 隐式绑定,当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。不过隐式绑定经常丢失this绑定
  3. 显示绑定,call和apply方法,第一个参数是对象,是为this准备的,如果是原始类型则会被装箱。硬绑定即bind方法,创建一个包裹函数,返回一个显示指定上下文的函数。相应的还有API调用,如forEach(foo,obj)//把this绑定到obj
  4. new绑定,使用new时对函数的构造调用会创建一个全新的对象,这个新对象会绑定到函数调用的this,如果函数没有返回其他对象那么自动返回这个新对象。

绑定例外:

  • 如果函数中确实使用了this,而传入了null,那么this将绑定到全局对象,可能造成不可预计的后果。更安全的this使用Object.create(null)创建一个新的对象,它和{}很像,不过不会创建一个Object.prototype这个委托

关于ES6箭头函数中的this

箭头函数的this的规则跟function定义的this不一样,箭头函数的this是在定义函数的时候绑定,而不是执行函数的时候绑定。 我们可以对比一下:

var x=11;
var obj={
  x:22,
  say:function(){
    console.log(this.x)
  }
}
obj.say();
//22
var x=11;
var obj={
 x:22,
 say:()=>{
   console.log(this.x);
 }
}
obj.say();

所谓的定义时候绑定,就是this是继承自父执行上下文!!中的this,比如这里的箭头函数中的this.x,箭头函数本身与say平级以key:value的形式,也就是箭头函数本身所在的对象为obj,而obj的父执行上下文就是window,因此这里的this.x实际上表示的是window.x,因此输出的是11。 需要强调的是箭头函数的this指向的是父执行上下文

模拟实现call和apply

call方法在使用一个指定的this值和若干个指定的参数值的前提下调用某个函数或方法
先看看看call的用例:

var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

bar.call(foo); // 1

所以我们可以看到,call函数有两个作用

  • 将this的值指向foo
  • 执行了bar函数。 而且还有两个要求:
  • 当this传入参数为null的时候,this指向window
  • call函数可以拥有返回值 所以根据上面this的指向讨论,我们的思路就可以变为:
  1. 把函数设成对象的属性
  2. 执行该函数
  3. 删除该属性 如下:
Function.prototype.MyCall =  function(contex){
    context.fn = this;//this指向调用call方法的函数
    context.fn();
    delete context.fn;
}

对于call函数可以根据给定参数指向函数的特性,我们可以通过arguments收集参数;

var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
    args.push('arguments[' + i + ']');
}

所以我们的最终版函数就是:

//最终版函数
Function.prototype.MyCall =  function(contex){
    context = context || window;
    context.fn = this;//this指向调用call方法的函数
    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    let result = eval('context.fn('+args+')');//eval自动把args转化为字符串。
    delete context.fn;
    return result;//call函数可以有返回值
}

apply与call函数类似,区别在与第二个参数是个数组。

//最终版函数
Function.prototype.MyApply =  function(contex,arr){
    context = context || window;
    context.fn = this;//this指向调用call方法的函数
    let result;
    if(!arr){
        result = context.fn()
    }else{
        var args = [];
        for(var i = 0, len = arr.length; i < len; i++) {
            args.push('arguments[' + i + ']');
        }
        result = eval('context.fn(' + args + ')')
    }
    delete context.fn;
    return result;//call函数可以有返回值
}

模拟实现bind

bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数

关于this,我们可以直接通过call或者apply来实现。

Function.prototype.MyBind = function (context){
    const self = this;
    return function (){
        return self.apply(context)//函数可以有返回值
    }
}

对于参数可以使用arguments来实现

Function.prototype.MyBind = function (context){
    const self = this;
    //bind函数的参数
    const args = Array.prototype.slice.call(arguments,1);
    return function (){
        //生成函数运行时候的参数
        const bingArgs = Array.prototype.slice.call(arguments);
        return self.apply(context,args.concat(bingArgs))//函数可以有返回值
    }
}

bind还有个最重要的特点:一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。
当bind返回的函数作为构造函数的时候,bind的this失效,但是传入的参数依然可用。
这个时候我们需要通过修改原型来实现这个方法:

//最终版函数
Function.prototype.MyBind = function (context){
    if(typeof this !== "function"){
        throw new Error("绑定this必须是个function")
    }
    var self = this;
    //bind函数的参数
    var args = Array.prototype.slice.call(arguments,1);
    var fNOP = function() {};//用来中转,避免修改fBound的prototype的时候,也会修改绑定函数的prototype;
    var fBound = function (){
        //生成函数运行时候的参数
        var bingArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP?this:context,args.concat(bingArgs))//函数可以有返回值
    }
    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}

参考文档: