JavaScript框架设计笔记 · 系列 第四章 类的原理

58 阅读4分钟

类 · 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类的基本原理了。请持续关注本栏目。
码字不易,动动你可爱的手指头点个赞再走呗。