You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
但是这个类的实现方式也有一定问题,比如没有办法设置原型,这样也就无法实现继承。
因此,在 JavaScript 中,一般使用具有 this 的构造函数和 new 操作符来模拟类和对象,这样也是更符合传统面向对象语言开发者直觉的设计。
functionPerson(name,age){this.name=name;this.age=age;}Person.prototype.say=function(){console.log('my name is ',name);}consttom=newPerson('tom',23);constjerry=newPerson('jerry',24);
当然,你也许会困惑,当使用 new 的时候,它到底做了什么呢?
其实 new 操作符做的事情很简单,大致就是以下几步:
前言
JavaScript 在设计之初,是一门具有函数式、面向对象( OOP )等风格的多范式语言。
虽然 JavaScript 中到处都是“对象”,但如果想要 JavaScript 中使用类,在 ES5 之前需要使用构造函数和原型( prototype )来模拟类,而涉及到继承的时候,实现方式之多更是让人眼花缭乱。
因此,在 ES6 之后,JavaScript 增加了 class 和 extends 关键字,这让JavaScript更加接近传统的面向对象语言,也方便了开发。
1. 类
什么是类呢?这里引用一下维基百科的解释:
类是对现实生活中一类具有共同特征的事物的抽象,像老鼠、猫、人类等都可以作为类。但具体的个体就是对象,比如一只叫汤姆的猫,一只叫杰瑞的老鼠。
在 JavaScript 中每个对象都可以都是基于引用类型创建的,这个引用类型可以是原生类型( Date、String、Boolean 等),也可以是自定义的类型。因此,类也可以被理解为是描述数据和行为的一种复杂类型。
1.1 ES5 中的类
在开始之前,我们先基于上述的描述来实现一个简单的类。
由于类是一种自定义的复杂类型,封装了一类事物的共同特性,而对象只是类返回的一个实例,那么我们完全可以考虑使用函数来创建,最后返回一个对象。
以下面这个人的类为例:
但是这个类的实现方式也有一定问题,比如没有办法设置原型,这样也就无法实现继承。
因此,在 JavaScript 中,一般使用具有 this 的构造函数和 new 操作符来模拟类和对象,这样也是更符合传统面向对象语言开发者直觉的设计。
当然,你也许会困惑,当使用 new 的时候,它到底做了什么呢?
其实 new 操作符做的事情很简单,大致就是以下几步:
obj
this
指向obj
obj
的[[Prototype]]
指针指向构造函数的原型prototype
按照上面这四步,可以自己手动实现一个 new 操作符。
调用方式也比较简单:
通过myNew和new最后得到的两个实例,两者表现是一致的,[[Prototype]]也都指向了同一地址。
[[Prototype]]
是存在于实例对象上的一个指针,它指向构造函数的原型( prototype ),通过[[Prototype]]
连接起一个个对象,最终形成一条原型链,原型链的终点是Object.prototype.__proto__
,也就是null。__proto__
,而建议用Object.setPrototypeOf
呢?这是因为
__proto__
并非官方标准,而是浏览器自己实现的,可能会出现兼容性问题,而Object.setPrototypeOf
则是 ES6 中标准,未来所有浏览器都会支持。上述代码原型链关系图:
1.2 ES6 class
在 ES6 中新加入了
class
的语法糖,从此和手动模拟类的时代说拜拜。可以明显地看到,原来的
Person
构造函数,现在变成了constructor
。而在Person
类中直接定义的方法,会被绑定到原型上,我们也不需要再用Person.prototype.say
这种复杂方式来定义原型方法。用新增加的
static
关键字定义的属性代表着类上面的静态属性,也取代了原有的Person.name
的形式(当然,在 ES6 中依然可以用Person.name
来定义静态属性)。除以之外,使用
class
定义的类,无法像原来的构造函数一样直接调用,否则会报错。2. 继承
继承是指可以在不编写更多代码的情况下,一个类可以使用另一个类上的属性或者方法。甚至父类可以只提供接口,让子类去实现。
一般来说,继承的类叫做「子类」或者「派生类」,而被继承的类叫做「父类」或者「超类」。
2.1 extend
前面我们说过,在 JavaScript 中到处都是“对象”,而对象也是基于引用类型创建的。聪明的孩子也许会想到,能不能将不同的对象进行混合,从而实现类似继承的效果呢?
在 jQuery、underscore/lodash、Backbone 等框架和库中都提供了类似扩展( extend )的功能,直接将对象的属性合并到目标对象中。
这里使用了 jQuery 中的
extend
方法来将map
对象分别合并到baiduMap
和googleMap
上,最终两者都具有了前者的方法。在 ES6 中也提供了新的方法
Object.assign
来实现对象的合并,用法也和$.extend
几乎一致。但不管
extend
还是Object.assign
都只是浅拷贝,如果将引用类型属性合并到不同的目标对象中,一旦其中一个目标对象修改了这个属性,就会造成其他目标对象也跟着变化。2.2 mixin
而在 Vue 和 早期的 React 中都支持一种名为 mixin 的方式,mixin的作用在于将不同组件的共同部分抽取出来,实现逻辑的复用。
Vue 中的 mixin(将
toggle
提取出来可以供不同组件使用):React 早期的 mixin(将设置默认的props提取出来):
甚至在 sass 和 less 这些 css 预处理器中也支持 mixin 的形式,允许我们灵活复用相同的 css 代码。
scss 中的 mixin(封装了
border-radius
):2.3 基于原型链的继承
extend
和mixin
在很多框架和库中被使用,但两者本质上都是做了对象的合并,适用范围有限。在 ES6 出现之前,如何用 ES5 实现继承也也是前端面试中的常见题型之一,这里将重点介绍组合继承和寄生组合继承。
2.3.1 组合继承
通过原型链的机制,将父类的实例赋值给子类的原型链来实现子类继承父类的属性。
同时,又将父类的构造函数在子类的构造函数中执行,来实现绑定父类构造函数中的属性。
原型链关系图:
但是组合继承有个问题,就是在设置
Child
的原型时,需要实例化一次Parent
构造函数,导致了Parent
构造函数被调用了两次。2.3.2 寄生式继承
寄生式继承的思路和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数内部以某种方式来增强对象,最后返回这个对象。
寄生式继承更像是将对象与对象衔接起来,形成一条原型链。
2.3.2 寄生组合式继承
寄生组合式继承是将寄生式继承与组合继承结合起来的一种继承方式,主要是用
Object.create
来代替原来实例化父构造函数,它解决了组合继承中调用两次父构造函数的弊端,也是最理想的继承范式。原型链关系图:
《JavaScript高级程序设计》中对寄生组合式继承的评价是这样的:
2.4 ES6 继承
在 ES6 中,新增加了
extends
的语法糖,在定义子类的时候可以直接继承父类,这样统一了继承的方式,让大家不再被各种各样的继承方式困扰。2.5 多重继承
多重继承是指一个子类同时继承多个父类,拥有这多个类的属性和方法。由于 JavaScript 的继承是基于原型链的,原型链一般只有一条链,无法同时指向多个不同的对象,因此 JavaScript 中是无法实现传统的多重继承。
但是可以让父类分别互相继承,子类继承最后那个父类来实现多重继承。这种实现方式的缺点就是要在每个父类定义的时候继承另一个父类。
另一种实现方式,就是我们前面提到过的
mixin
,mixin
不仅在各大框架中被广泛使用,也可以将多个父类进行混合,从而实现多重继承的效果。使用方式:
推荐阅读
The text was updated successfully, but these errors were encountered: