Javascript OO 实现

2016/03/08 ooclass

# 关键字

理解三个关键字:

  1. function: JS 世界里 Class 的定义用 functionfunction 里面的内容就是构造函数的内容
  2. this: 代表调用这个函数的对象
  3. prototype: 用它来定义成员函数,比较规范和保险

简单例子:

// 定义 Circle 类,拥有成员变量 r,常量 PI 和计算面积的成员函数 area()
function Circle(radius) {
    this.r = radius;
}

Circle.PI = 3.1415926;
Circle.prototype.area = function() {
    return Circle.PI * this.r * this.r;
}

// 使用 Circle 类
var c = new Circle(1.0);
console.log(c);

另外成员函数也可以写成这样:

function compute_area() {
    return Circle.PI * this.r * this.r;
}

Circle.prototype.area = compute_area;

# 创建对象

# 1. 工厂模式

function createPerson(name, age, job) {
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function() {
        console.log(this.name);
    }

    return o;
}

var person1 = createPerson('jimco', 24, 'Front-end Engineer');
var person2 = crestePerson('Lucy', 25, 'Dcotor');

# 2. 构造函数模式

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function() {
        console.log(this.name);
    }
}

var person1 = new Person('jimco', 24, 'Front-end Engineer');
var person2 = new Person('Lucy', 25, 'Dcotor');

创建 Person 的新实例,必须使用 new 操作符,以这种方式调用构造函数实际上会经历以下 4 个步骤:

  • 创建一个新对象
  • 将构造函数的作用域赋给新对象(因此 this 就指向这个新对象)
  • 执行构造函数中的代码(为这个对象添加属性)
  • 返回新对象

# 3. 原型模式

function Person() {}

Person.prototype.name = 'jimco';
Person.prototype.age = '24';
Person.prototype.job = 'Front-end Engineer';
Person.prototype.sayName = function() {
    console.log(this.name);
};

var person1 = new Person();
var person2 = new Person();
console.log(person1.sayName == person2.sayName); // true

更简单的原型语法:

function Person() {}

Person.prototype = {
    constructor: Person,
    name: 'jimco',
    age: 24,
    job: 'Front-end Engineer',
    sayName: function() {
        console.log(this.name);
    }
}

# 4. 组合使用构造函数模式和原型模式

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ['Shelby', 'Court'];
}

Person.prototype = {
    constructor: Person,
    sayName: function() {
        console.log(this.name);
    }
}

var person1 = new Person('jimco', 24, 'Front-end Engineer');
var person2 = new Person('Lucy', 25, 'Dcotor');

person1.friends.push('Van');

console.log(person1.friends);
console.log(person2.friends);
console.log(person1.friends === person2.friends); // false
console.log(person1.sayName === person2.sayName); // true

# 继承

基本思想:利用原型让一个引用类型继承另一个引用类型的属性和方法。

每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针(constructor),而实例都包含一个指向原型对象的内部指针。那么,假如我们让原型对象等于另一个类型的实例,显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,如此层层递进,就构成了实例与原型的链条。这就是原型链的基本概念。

# 1. 原型链继承

  • 原理:将父类实例作为子类原型
  • 优点:方法复用
  • 缺点
    • 父类上引用类型的属性,会被所有实例共享
    • 创建子类实例时,无法向父类构造函数传参
function SuperType() {
    this.property = true;
}

SuperType.prototype.getSuperVal = function() {
    return this.property;
}

function SubType = {
    this.subproperty = false;
}

SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType; // 若不重写则指向 SuperType

SubType.prototype.getSubVal = function() {
    return this.subproperty;
}

var instance = new SubType();
console.log(instance.getSuperVal()); // true

包含引用类型值的属性会被所有实例共享:

function SuperType() {
    this.colors = ['red', 'green', 'blue'];
}

function SubType() {}

SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push('black');
console.log(instance1.colors); // ['red', 'green', 'blue', 'black']

var instance2 = new SubType();
console.log(instance2.colors); // ['red', 'green', 'blue', 'black']

# 2. 构造函数继承

  • 原理:通过在子类构造函数的内部调用父类的构造函数,从而实现继承
  • 优点:实例之间独立
  • 缺点
    • 父类方法不能复用(由于方法在父构造函数中定义,每次创建子类实例都要创建一遍方法,导致方法不能复用)
    • 子类只能拿到父类的属性和方法,无法拿到父类原型的属性
function SuperType() {
    this.colors = ['red', 'green', 'blue'];
    this.getColors = function() {
        console.log(this.colors);
    }
}

function SubType() {
    // 继承了 SuperType
    SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push('black');
console.log(instance1.colors); // ['red', 'green', 'blue', 'black']

var instance2 = new SubType();
console.log(instance2.colors); // ['red', 'green', 'blue']

# 3. 组合继承

  • 原理:通过调用父类构造函数,继承父类的属性并保留传参的优点;然后通过将父类实例作为子类原型,实现函数复用
  • 优点
    • 保留构造函数的优点:创建子类实例,可以向父类构造函数传参数
    • 保留原型链的优点:父类的实例方法定义在父类的原型对象上,可以实现方法复用
    • 不共享父类的引用属性
  • 缺点:无论在什么情况下,都会调用 2 次父类构造函数,一次是在创建子类原型的时候,另一次是在子类构造函数内部
    • 第一次 SuperType.call(this); 从父类拷贝一份父类实例属性,作为子类的实例属性
    • 第二次 SubType.prototype = new SuperType() 创建父类实例作为子类原型,此时这个父类实例就又有了一份实例属性,但这份会被第一次拷贝来的实例属性屏蔽掉,所以多余

注意:使用「组合继承」时要记得修复 SubType.prototype.constructor 指向

function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'green', 'blue'];
}

SuperType.prototype.sayName = function() {
    console.log(this.name);
}

function SubType(name, age) {
    SuperType.call(this, name); // 继承属性
    this.age = age;
}

SubType.prototype = new SuperType(); // 继承方法
SubType.prototype.constructor = SubType;

SubType.prototype.sayAge = function() {
    console.log(this.age);
}

var instance1 = new SubType('jimco', 24);
instance1.colors.push('black');
console.log(instance1.colors); // ['red', 'green', 'blue', 'black']
instance1.sayName();           // 'Jimco'
instance1.sayAge();            // 24

var instance2 = new SubType('Lucy', 25);
console.log(instance2.colors); // ['red', 'green', 'blue']
instance2.sayName();           // 'Lucy'
instance2.sayAge();            // 25

# 4. 组合继承2

  • 原理:通过调用父类构造函数,继承父类的属性并保留传参的优点;然后通过将父类原型作为子类原型,实现函数复用
  • 优点
    • 只调用一次父类构造函数
    • 保留构造函数的优点:创建子类实例,可以向父类构造函数传参数
    • 保留原型链的优点:父类的实例方法定义在父类的原型对象上,可以实现方法复用
  • 缺点:在修正子类构造函数的指时,父类实例的构造函数指向同时也发生变化(这是我们不希望的)
function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'green', 'blue'];
}

SuperType.prototype.sayName = function() {
    console.log(this.name);
}

function SubType(name, age) {
    SuperType.call(this, name); // 继承属性
    this.age = age;
}

SubType.prototype = SuperType.prototype; // 继承方法
SubType.prototype.constructor = SubType;

SubType.prototype.sayAge = function() {
    console.log(this.age);
}

const p1 = new SuperType('jim);

console.log(p1.constructor); // SubType

# 5. 原型式继承

  • 原理:原型式继承的 object 方法本质上是对参数对象的一个浅复制
  • 优点:父类方法可以复用
  • 缺点
    • 父类的引用属性会被所有子类实例共享
    • 子类构建实例时不能向父类传递参数
function object(o) {
    function F() {};
    F.prototype = o;
    return new F();
}

var person = {
    name: 'jimco',
    friends: ['Shelby', 'Court', 'Van']
}

var person1 = object(person);
person1.name = 'Greg';
person1.friends.push('Bob');

var person2 = object(person);
person2.name = 'Linda';
person2.friends.push('Barbie');

console.log(person2.friends); // ['Shelby', 'Court', 'Van', 'Bob', 'Barbie']

object() 方法与 ECMAScript 5 新增的 Object.create() 方法类似。

在没有必要兴师动众地创建构造函数的,而只想让一个对象与另一个对象保持类似的情况下,原型式继承完全是可以胜任的。不过,包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。

# 6. 寄生式继承

  • 原理:使用原型式继承获得一个目标对象的浅复制,然后增强这个浅复制的能力
  • 优点:-
  • 缺点:与构造函数模式类似,方法无法复用
function createAnother(original) {
    var clone = object(original);   // 通过调用函数创建一个新对象
    clone.sayHi = function() {      // 以某种方式来增强这个对象
        console.log('hi');
    }

    return clone; // 返回这个对象
}

var person = {
    name: 'jimco',
    friend: ['Shelby', 'Court', 'Van']
}

var person1 = createAnother(person);
person1.sayHi(); // 'hi'

# 7. 寄生组合式继承

  • 原理:通过调用父类构造函数,继承父类的属性并保留传参的优点;然后通过将父类原型的拷贝作为子类原型,实现函数复用(本质是对组合继承2缺点的优化改进)
  • 优点:最理想的继承范式
  • 缺点:-
// 同 Object.create
function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

function inheritPrototype(subType, superType) {
    var prototype = object(superType.prototype);
    prototype.constructor = subType;
    subType.prototype = prototype;
}

function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'green', 'blue'];
}

SuperType.prototype.sayName = function() {
    console.log(this.name);
}

function SubType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}

inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function() {
    console.log(this.age);
}

这个例子的高效率体现在它只调用了一次 SuperType 构造函数,并且避免了在 SuperType.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此还能够正常使用 instanceofisPrototypeOf()

# 8. ES6 继承

通过 Class 的 extends + super 实现继承。

ES6 规定,子类必须在 constructor() 方法中调用 super(),否则就会报错。这是因为子类自己的 this 对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,添加子类自己的实例属性和方法。如果不调用 super() 方法,子类就得不到自己的 this 对象。

class Point { /* ... */ }

class ColorPoint extends Point {
    constructor() {
    }
}

let cp = new ColorPoint(); // ReferenceError

构造函数没有调用 super(),导致新建实例时报错。

为什么子类的构造函数,一定要调用 super()?原因在于 ES6 的继承机制,与 ES5 完全不同。ES5 的继承机制,是先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即「实例在前,继承在后」。ES6 的继承机制,则是先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例,即「继承在前,实例在后」。这就是为什么 ES6 的继承必须先调用 super() 方法,因为这一步会生成一个继承父类的 this 对象,没有这一步就无法继承父类。

在子类的构造函数中,只有调用 super() 之后,才可以使用 this 关键字,否则会报错。这是因为子类实例的构建,必须先完成父类的继承,只有 super() 方法才能让子类实例继承父类。

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
}

class ColorPoint extends Point {
    constructor(x, y, color) {
        this.color = color; // ReferenceError
        super(x, y);
        this.color = color; // 正确
    }
}

上面代码中,子类的 constructor() 方法没有调用 super()之前,就使用 this 关键字,结果报错,而放在 super() 之后就是正确的。

如果子类没有定义constructor() 方法,这个方法会默认添加,并且里面会调用 super()。也就是说,不管有没有显式定义,任何一个子类都有constructor() 方法。

class ColorPoint extends Point {
}

// 等同于
class ColorPoint extends Point {
    constructor(...args) {
        super(...args);
    }
}

除了私有属性,父类的所有属性和方法,都会被子类继承,其中包括静态方法。

class Foo {
    #p = 1;
    #m() {
        console.log('hello');
    };
    static hello() {
        console.log('hello world');
    }
}

class Bar extends Foo {
    constructor() {
        super();
        console.log(this.#p); // 报错
        this.#m(); // 报错
    }
}

# 参考资料

上次更新: 2022/6/21 10:24:01