类 · class
本博文将从JavaScript类发展路径,继承分类以及类的实现原理进行阐述。另外也会大概讲讲属性描述符。那我们开始吧。
new Function及继承原理
在JavaScript推出class特性以前,使用普通function及关键字new来实现类似于类的功能:
// age是私有属性
function Person (name) {
var age = 18
this.name = name
this.getAge = function () {
return age;
}
}
var p1 = new Person('xiaoming')
p1.getAge() // 18
p1.name // xiaoming
而这种实现,可以通过原型链的方式实现继承,很多人对于原型链有点混乱,这里岔开话题总结一下:
原型链是JavaScript语言的一种设计,使得js对象可以使用其继承的属性或方法。具体来说就是,如果你在一个对象上访问属性或方法,而它并不拥有这样的属性或方法,js引擎则往其原型指向的对象上去找,一直往上,直到object。
那么对象的原型怎么找呢?只需要记住一句话:
对象的原型,指向对象的构造函数的prototype属性,一直向上,构成原型链
怎么理解呢?我们举两个例子,第一个是我们平常使用的数组[], 它实际上是一个Array实例,而我们使用像slice之类的数组方法,在这个实例上是不存在的,那么调用的时候,我们调用的是谁的方法呢?没错,是Array.prototype上定义的方法,且看:
let arr = [] // let arr = new Array()
arr.slice() // 调用Array.prototype.slice.apply(arr)
再来举例,以第一个代码块的例子来看,实例p1的原型__proto__属性指向哪里呢?指向其构造函数Person(){}的prototype属性,也就是Person.prototype,由于在Person方法里定义的内部方法(例如getAge)会在每次实例化的时候都分配内存,也就是:
var p1 = new Person('xiaoming')
var p2 = new Person('xiaohong')
p1.getAge === p2.getAge // false
因此为了节约内存空间,可以把方法放进Person.prototype,这样一来,所有Person的实例都将共享且不分配新的内存空间:
function Person (name) {
this.name = name
}
Person.prototype.getAge = () => {
return 18
}
var p1 = new Person('xiaoming')
var p2 = new Person('xiaohong')
p1.getAge === p2.getAge // true
理解了new Function的原型链回溯及两种“类方法”的形式,我们顺水推舟来讲讲他的继承。
啥是继承呢?简单来说,比如我们有一个Animal的类,它拥有动物都需要的eat方法和run方法,我们现在需要创建一个阿黄的实例,鉴于阿黄是一只狗,他还需要bark的方法,显然,我们需要一个拥有bark方法的Dog类,同时拥有Animal的eat和run方法,这就要求Dog类继承Animal类。
我们先把这两个类写一下,这里使用上述两种方式分别定义eat和run方法,
然后按上面的方法实现一下原型继承:
- 原型继承
function Animal(name) {
this.name = name
this.eat = function() {
console.log(`${this.name} eat`)
}
}
Animal.prototype.run = function(){
console.log(`${this.name} run`)
}
function Dog(name) {
this.name = name
this.bark = function() {
console.log(`${this.name} bark`)
}
}
Dog.prototype = Object.create(Animal.prototype, {})
let ahuang = new Dog('ahuang')
ahuang.bark()
ahuang.run()
ahuang.eat()
// Object.create内部实现大致如下
// 就是实例化首参数的构造器
Object.create = function (o) {
function F () {}
F.prototype = o
return new F()
}
这里使用了Dog.prototype = Object.create(Animal.prototype, {}),也就是ahuang实例的构造函数Dog的prototype指向了一个Animal的实例, Dog.prototype instanceof Animal会返回true。注意,由于Dog.prototype是Animal的一个实例,Dog.prototype的构造方法就是Animal,这样ahuang的原型链就是:
ahuang --> Dog.prototype --> Animal.prototype --> Object.prototype
可以看到bark和run方法都没有问题,说明我们的原型继承是OK的,Dog类继承了Animal的run方法。
但是,eat方法在调用的时候报错了。这是为啥?
显然,eat方法定义在Animal的方法内部而不是prototype上,阿黄的原型链上并不存在这个方法,这里就引出我们常用的第二种继承方法:构造器继承。
- 构造器继承
在上面的例子中,Animal的eat方法定义在Animal内部,无法被基于原型链继承的Dog继承,既然在构造函数里,那么我们直接调用需继承的构造函数,并且替换掉this指向就好啦:
function Dog(name) {
Animal.apply(this, arguments)
this.name = name
this.bark = function() {
console.log(`${this.name} bark`)
}
}
完美。
至此,我们把class以前的JavaScript“类实现”捋了一遍,包括原型链及继承原理。接下来,我们可以开始讲讲es6推出的class特性。
class
鉴于科班的小伙伴教科书都是Java的,说到面向对象就想到class,那么new Function这种反直觉的实现你能忍?(异口同声:不能!!)好的,既然不能,ESCMAScript也就急人所急推出了class。
class Animal {
constructor (name) {
this.name = name
}
eat () {
console.log(`${this.name} eat`)
}
run () {
console.log(`${this.name} run`)
}
}
class Dog extends Animal{
constructor (name) {
super(name)
this.name = name
}
bark () {
console.log(`${this.name} bark`)
}
}
let ahuang = new Dog('ahuang')
ahuang.eat()
ahuang.bark()
新的class特性只需要extends关键字就实现了类的继承,和Java一毛一样。这里注意,使用了extends继承,就必须在构造函数里面super实例化一下父类,以创建当前的this,否则不能使用this,如上,如果没有super,就不能使用this.name = name,否则就会报错。
值得说的是,这样的实现方式,他的底层原理仍然是函数,并且继承原理依然是原型链继承,有的小伙伴会问:就没有构造器继承了吗?是的,没有。因为class里面定义的类方法,实际上都是定义在构造函数的prototype上,也就是:
class Animal {
constructor (name) {
this.name = name
}
eat () {
console.log(`${this.name} eat`)
}
run () {
console.log(`${this.name} run`)
}
}
// 等同于
function Animal () {}
Animal.prototype.constructor = function() {}
Animal.prototype.eat = function() {}
Animal.prototype.run = function() {}
既然方法都在原型上,那么只需要使用原型继承即可。来看看实现层面的代码,extends事实上是这样的:
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
// Object.create(proto, propertiesObject) 方法
// 创建一个新对象,使用 proto 来提供新创建的对象的__proto__
// 将 propertiesObject 的属性添加到新创建对象的不可枚举(默认)属性(即其自身定义的属性,而不是其原型链上的枚举属性)
subClass.prototype = Object.create(superClass && superClass.prototype,
{ constructor: { value: subClass, writable: true, configurable: true } });
if (superClass) _setPrototypeOf(subClass, superClass);
}
// 设置对象 o 的原型(即 __proto__ 属性)为 p
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; };
return _setPrototypeOf(o, p);
}
相信看到这里,你已经完全掌握了JavaScript类的基本原理了。请持续关注本栏目。
码字不易,动动你可爱的手指头点个赞再走呗。