Es5中的this和Es6箭头函数中的this随笔

777 阅读8分钟

前言

在上篇的# 从事件循环机制深挖到Promise 源码实现 中,涉及到了有些复杂的作用域知识。有朋友线下问我,有些看不懂。从跟ta的分析中,感觉其实是在这种复杂场景下,this的作用域有些不清晰了。

其实,这并不是什么不好的事,也不代表是'low',因为在大部分的业务代码中,很少有情况会出现这种递归调用某个类,然后还要进行this的处理。很长时间不用那么复杂的,或者压根就没有用到那么复杂的,那么出现这种问题非常正常。

1. 出题

下面我出两个题, 如果您能够很快看出来,那说明您对this作用域链的理解比较清晰,可以略过后面的篇幅。如果看着很费劲,那么有必要详细的读一下我这篇文章,一定会对你理解this大有帮助。

1.1 箭头函数方式

let flag = 0;
class MyClass {
    light = ++flag;
    constructor(fn) {
        const handle = () => {
            console.log('context...', this);
        }

        this.then= () => {
            console.log('this....', this);
            return new MyClass(() => {
                console.log('second this...', this);
                handle()
            })
        }
        fn();
    }
    
}
new MyClass(() => {

}).then();

如果你看过我的# 从事件循环机制深挖到Promise 源码实现, 会发现,其实这个函数就是我提取出来的一个简化版。

好了,大家思考30s,别急着往下看,心中有个结论后,再向下看。

用es5的方式

function MyTest(fn) {
    function abc() {
        console.log('abc', this);
    }
    this.scope = function() {
        console.log('scope-this', this);
        return new MyTest(function() {
            console.log('second,,,', this);
            abc();
        })
    }
    fn();
}

new MyTest(function() {
    console.log(this);
}).scope();

同样,停留30s钟,做到心中有个结论。

我想,读者中的绝大多数心中一定会咯噔一下,因为会感觉貌似没有那么的清晰。

其实我在写从事件循环机制深挖到Promise 源码实现 的代码时, 也心里咯噔了一下,感觉自己貌似对this不那么的了解了。 也是很长时间没写复杂代码,自己生疏了(写一些复杂逻辑代码真的很重要)。

好了, 大家如果有兴趣,可以复制一下代码,在控制台执行一下看看。

2. 从源头讲起

this的出现,大家或多或少的能够知道一些背景, 或者网上搜一下也有一大堆。 我再讲一下,可能比网上有些人讲的要稍微透彻一点点(见仁见智)。

要讲this, 就要讲作用域链。要讲作用域链, 那就要讲js代码在内存中的存储,这与数据结构相关。

基本数据类型这里简单说,基本数据类型是直接存储在栈中的,比如: a= 123,就是一个内存中有一个值123,这个内存地址是'0x123'。当新增一个变量 b=a时, 会重新开辟一块区域,把0x123中的内容复制过去,这时,b变量对应的内存地址是 '0x131'

我们今天要谈的是引用类型的Function,因为我们一般是在function中操作this的,在非function中没有什么好讲的。

看下面的这两段代码:

   function one () {}
   function two(){}

  function one() {
    function two() {

    }
 }

这两种其实在内存中创建没什么不同。 因为函数在创建时,都会在内存中开辟独立的空间来存储。比如,现在函数one的地址是 '0x234', 函数two的地址是 '0x334'

然后我们在代码中引用:

    var name = 'init';
    function one () {console.log(this.name)}
    var obj = {
        name: 'zhangsan',
        part: one
    }
    
    obj.part(); // zhangsan 
    
    one() // init
    

javascript 允许函数内部引用当前执行环境(上下文)中的其它变量,这也就是我们说的作用域链。 我们也知道了函数是在自己单独的内存块中,所以可以在任何环境对它进行调用。这就有问题了,我们怎么知道它的上下文在哪? 他的调用太灵活了。

所以,JavaScript就规定用一种机制可以在函数体内获得当前的执行环境,也就是上下文, 那么我们就可以获取到这个上下文中的其它变量了。这里是通过v8的c++部分处理的, 所以this就出现了。

基本原理说完了,但是,还需要很多例子才能真正理解,就像一条数学公理,不多运用,是无法真正理解的。

那么我们开始用各种例子来理解这条'公理'。

2.1 举例

例子1

    var name = 'init';
    function one () {console.log(this.name)}
    var obj = {
        name: 'zhangsan',
        part: one
    }
    
    obj.part() // 'zhangsan'
    var fn = obj.part;
    fn() // 执行结果是什么??
    

fn()执行的结果真不是 ’zhangsan‘, 而是window。 这里很多人都会记一个小公式: 看函数前面有没有点,在es5中确实可以这么看,但是更要知道其中的原理:fn是一个新的变量,他的内存地址比如是 '0x555', 这个内存地址中直接存储了one函数的内存地址’0x666‘, fn直接指向了one函数。

而obj.part(), 是需要通过obj才能找到part,所以根据作用域链,part中的this,指向的是obj

例子2

继续向下看例子:

function MyTest(fn) {
    console.log('init-this', this);
    function abc() {
        console.log('abc', this);
    }
    this.scope = function() {
        console.log('scope-this', this);
        abc();
    }
    fn();
}

new MyTest(function() {
    console.log('callback-this:', this);
}).scope();

这个例子就复杂的多了。我们一点点分析:

  • 实例话构造函数的时候, init-this 对应的是MyTest实例,因为new 构造函数的时候,其中的一步就是将构造函数执行环境的this指向新实例。
  • callback-this 指向的是window, 这是因为这就是一个单纯的回调函数,并没有其它的变量引用它。这里需要注意一下。
  • scope-this 同样指向的也是 MyTest实例,因为触发scope函数的时机是 实例.scope()
  • abc指向的是 window, 同样也是因为没有别的变量引用它。

那么如果想让this的指向一致怎么办? 那就需要改变函数中的运行环境了,通过什么呢? call/apply/bind都可以。

例子3

将上面的例子2改动一下,通过call,改变this指向的运行环境(上下文)

function MyTest(fn) {
    console.log('init-this', this);
    function abc() {
        console.log('abc', this);
    }
    this.scope = function() {
        console.log('scope-this', this);
        abc.call(this);
    }
    fn.call(this);
}

new MyTest(function() {
    console.log('callback-this:', this);
}).scope();

例子4

来到刚开始的例子:

function MyTest(fn) {
    function abc() {
        console.log('abc', this);
    }
    this.scope = function() {
        console.log('scope-this', this);
        return new MyTest(function() {
            console.log('second,,,', this);
            abc();
        })
    }
    fn();
}

new MyTest(function() {
    console.log(this);
}).scope();

现在我们应该清楚执行结果拉:

  • scope-this指向 MyTest实例
  • 其他都指向window

3.ES6的箭头函数

估计大家现在用ES6的写法比较多了,毕竟现在很多项目都是单页应用,同样也有各种构件工具通过babel转成ES5

ES6中有很方便的地方,也有存在遗憾的地方。 今天的箭头函数就是很方便的地方。

ES5的this问题,其实业界多有诟病,因为它太灵活,太不稳定,稍不小心,稍微理解的浅一些就容易掉坑里。 ES6的箭头函数让this稳定了下来。这还要得益于ES6是静态编译的,所以在静态编译时,执行环境就能够稳定下来。

网上介绍很多,我不多说了,主要通过例子来证明: 箭头函数的this不是在运行时才确定作用域的,而是在定义的时候就生成了具体的作用域

我通过跟上面es5的例子做个对比:

3.1 举例

例子1


    var name = 'init';
    const one = () => {console.log(this.name)}
    var obj = {
        name: 'zhangsan',
        part: one
    }
    
    obj.part() // 执行结果是什么?
    var fn = obj.part;
    fn() // 执行结果又是什么??

可以看到,两个的执行结果是一致的,都是'init', 这确实证明了函数内的this,它指向的作用域确实是在定义时就确定了。

继续!

例子2

function MyTest(fn) {
    console.log('init-this', this);
    const abc = () => {
        console.log('abc', this);
    }
    this.scope = () => {
        console.log('scope-this', this);
        abc();
    }
    fn();
}

new MyTest(()=> {
    console.log('callback-this:', this);
}).scope();

除了callback-this 指向window, 其它都指向了MyTest的实例。

例子3

let flag = 0;
class MyClass {
    light = ++flag;
    constructor(fn) {
        const handle = () => {
            console.log('context...', this);
        }

        this.then= () => {
            console.log('this....', this);
            return new MyClass(() => {
                console.log('second this...', this);
                handle()
            })
        }
        fn();
    }
    
}
new MyClass(() => {

}).then();

这个例子就是刚开始的例子了, 看到了这里,我相信剩下的难点只有then方法里又返回了MyClass的实例, 虽然有了之前的基础,但免不了还是不敢立马确定。

我加上了light变量,就是为了好区分各自作用域的。

  • this... 这,一定是返回{light:1, xxx},因为this.then() 并不是箭头函数,它指向的是实例。
  • second this...这,返回的也一定是{light:1, xxx}, 因为定义时,这个回调函数对应的作用域,也会向上找到this.then()所在的作用域, 一定不是{light:2, xxx}, 这里是难点
  • context...返回的也是{light:1, xxx}, 原因同上。

这里大家可以多琢磨琢磨,有些绕。

终极大boss

终极大boss在我的 # 从事件循环机制深挖到Promise 源码实现 中,里面有更为复杂的逻辑,涉及到不同层的环境作用域, 喜欢的可以仔细看看~~