js中的小疑惑 —— class中的箭头函数
最近在学习Node的过程中,遇到了一个小问题,在此稍稍总结一下
一、ES6之前如何创建类
尽管在ES6中class关键词已经得到全面支持,但在没有class的时候,早期版本的JavaScript是如何创建类的呢?在这里我们有必要了解,这样有帮助我们理解,在使用class关键字时,”底层”发生了什么。
使用构造函数进行创建
构造函数是一种专门用于初始化新对象的函数,构造函数要使用new关键字调用,因此构造函数本身只需要初始化新对象的状态。构造函数调用的关键在于构造函数的prototype属性将被用作新对象的原型。我们再来回忆下new关键字的底层原理吧(哈哈,貌似讲着讲着就变成回忆各种原理了,了解一下也有好处嘛)
new操作符的原理
1.创建一个新对象
2.空对象的内部属性 _proto_ 赋值为构造函数的 prototype 属性
3.将构造函数的this指向新对象
4.执行构造函数内部代码
5.返回该新对象
好了,扯了这么多,我们再来回来看看ES6之前一般是如何创建对象的
1 | function Cat(name){ |
我们创建每一个函数都有一个 prototype(原型)属性,指向一个对象。这个对象的用途是包含所有特定类型(例子是 Cat)的所有实例共享方法(sing)。
如果用ES6中的关键字class,那么上面代码就会变成这样
1 | class Cat { |
其实上面两种定义的类工作方式完全一样,理解这一点非常重要,class关键字并没有改变JavaScript类基于原型的本质,在class内部定义的普通函数(箭头函数比较特殊)会被添加到类的原型prototype中。class只能算作是一种语法糖,目的在于让程序员可以更快速地创建。
二、class中的箭头函数
上面我们说道,在class中使用普通函数时,普通函数会被添加到类的prototype上。但箭头函数士特殊的,当我们使用箭头函数时,箭头函数会被自动添加到构造函数constructor中。这两者有什么区别呢?我们接下来就来看一下
1 | //普通函数 |
当我们分别输出类的原型(Cat.prototype,Dog.prototype)时,
{constructor: ƒ, sing: ƒ}
{constructor: ƒ}
上面表明,箭头函数的确是不会添加到原型上的。事实上啊,箭头函数会被编译到constructor里面作为this.xxx = xxx 使用。这意味着,伴随每一次类的实例化,箭头函数都将同时被实例化一次。
于是,很多人觉得这样做会出现性能问题,毕竟添加到原型上只需创建一次,所有实例就可以共享添加的方法了。
三、js中的this问题
1.问题引入
那么,在class中使用箭头函数有什么用呢?既然它有性能问题,那么我们该如何解决呢。我们先来看一个案例
1 | //使用箭头函数 |
当我们运行上面的代码时,就会报错,无法正常执行。浏览器告诉我们无法从undefined上获取sing方法。这是怎么回事呢,我们稍稍修改一下,把class内部的sing方法的函数体,改为打印this。
1 | sing(){ console.log(this) } |
这时我们会惊讶的发现,this居然变成了this(但如果使用我们之前的方法,用“.属性”的方式,即jackDog.sing()方式调用,这时的this为实例本身)。
这是怎么回事呢?且往下看
每个函数都有一个隐式的
this
形参。将函数作为方法调用时,这个参数会被设置为用于访问该方法的对象。这和大多数[面向对象语言]中的this
(或self
)含义相同。但是 JavaScript 在「关联到对象的方法」与「独立函数」这两者之间,使用了单一的定义形式。这使this
导致了许多程序员的困惑和 bug。
2.函数与类中的this
看了上面的内容,是不是稍稍有点疑惑,没关系,我们再来简单的梳理一下
事实上,对于许多编程语言来说,只有类中有this,函数内部是没有thisD的。而js中每一个函数也有自己的this,于是这就造成了上面我们提到的问题。
- 函数中的this
js默认的普通函数this指向代用它的对象,如果没有调用他的对象,则其指向undefined或window(严格模式下指向undefined,非严格模式下指向window)。而箭头函数比较特殊,永远指向函数外的this。
- 类中的this
事实上, 类的方法内部如果有this,默认指向类的实例
这样我们也就能明白为什么上面this打印的内容为undefined了,由于sing()运行在全局中,其this为undefined,故出现了上述问题。进而,我们也就无法获取对应的方法或属性。我们来看一下JS创始人对此的一些说明。
每个函数都有一个隐式的
this
形参。将函数作为方法调用时,这个参数会被设置为用于访问该方法的对象。这和大多数[面向对象语言]中的this
(或self
)含义相同。但是 JavaScript 在「关联到对象的方法」与「独立函数」这两者之间,使用了单一的定义形式。这使this
导致了许多程序员的困惑和 bug。
3.解决方法:
2.1.使用箭头函数解决
1 | //使用箭头函数 |
我们分析一下原因,箭头函数中的this,只和定义该箭头函数的位置有关系,即箭头函数中的this始终是该箭头函数所在作用域中的this。在第二个部分,我们已经了解到在类中使用箭头函数时,伴随每一次类的实例化,箭头函数都将同时被实例化一次。故此处箭头函数所在的作用域中的this指向类的实例对象。
2.2.使用bind绑定解决
1 | class Dog { |
两种方法的对比:使用箭头函数时,其不会添加到类的原型中,相当于在每个类被实例时,会在实例中单独的添加一个对应的方法。而使用bind绑定法,类中所定义的方法,是被添加到类的原型中,这样所有的实例可以共享这个方法。所以,如果希望追求极致的性能,可以使用bind来解决class中的this问题。
当然,如果我们再来回到底层,探究箭头函数的原理。事实上,箭头函数底层相当于.bind(); 永远绑定外部this。