Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript 函数原型继承与 Classes #5

Open
bouquetrender opened this issue May 9, 2018 · 0 comments
Open

JavaScript 函数原型继承与 Classes #5

bouquetrender opened this issue May 9, 2018 · 0 comments

Comments

@bouquetrender
Copy link
Owner

红宝书面向对象这一章节看了不下两三遍去理解书中提到不同的继承方式和思考为何代码需要这样编写,这篇文章也算是第六章的阅读笔记。回忆了一下自己写过的代码非常非常少用面向对象方式去写。JavaScript 是类面向对象的语言,理解原型继承等等非常有必要,而 OOP 优点在于封装、代码重用。如果JS代码仅有100行左右的规模,多个纯 function 调用可行,可读性也许会比 OOP 更高,但如果代码量达到上千行,密密麻麻的都使用大量 function 调用的话那么将会极其难以维护。将松散的JS代码进行整合,便于后期的维护并让代码适应更多的业务逻辑。

构造函数

单独使用构造函数的问题

在构造函数中定义函数与声明函数在逻辑上是等价的,像下面这个例子,person 函数实例都会包含一个不同的 function 实例,这样导致每个实例不同的作用域链和标识符解析。且实例化两次函数没太大必要。单独使用构造函数情况十分少,相同的方法应该放在原型中。

function Person(name, age, job){
     this.name = name;
     this.age = age;
     this.job = job;
     this.sayName = function(){
         alert(this.name)
     }
     //this.sayName = new Function("alert(this.name)");
}

动态原型模式

动态原型是最优化的方式,将相同方法属性定义放在原型中,并用判断动态添加原型。

function Person(){
     this.name = name;
     this.age = age;
     this.job = job;
     if (typeof this.sayName != "function"){
          Person.prototype.sayName = function(){
               alert(this.name);
          };
     }
}

继承

ES5 继承共分六种继承方式

  • 原型链继承
  • 借用构造函数
  • 组合继承(原型链+借用构造函数)
  • 原型式继承
  • 寄生式继承
  • 寄生组合式继承

原型和实例的关系

第一种方式是使用 instanceof 操作符:

alert(instance instanceof Superfn); //true
alert(instance instanceof Subfn); //true

第二种方式是使用isPrototypeOf()方法:

alert(Superfn.prototype.isPrototypeOf(instance)); //true
alert(Subfn.prototype.isPrototypeOf(instance)); //true

原型链继承

下面的 Demo 代码,Subfn 继承 Superfn,原型链等于 Superfn 的实例,Subfn 原型 constructor 指针指向了另一个对象 Superfn 的原型。Superfn 原型指向 Superfn 构造函数。所以 Subfn 继承了 Superfn 原型与构造函数的方法与值。

function Superfn(){
     this.property = true;
}
Superfn.prototype.getSuperfnValue = function(){
     return this.property;
};

function Subfn(){
     this.Supproperty = false;
}
Subfn.prototype = new Superfn();

原型链存在的问题,一是例如当有引用类型值 colors 数组存在于被继承函数 Superfn 中,那么继承函数 Subfn 的每一个实例都共用 colors 。因为继承后 Subfn 的 colors 存在于 Subfn 原型中。二是不能向被继承函数 Superfn 传参数。

借用构造函数

实际上是 Subfn 利用 call 方法(或apply)在 Subfn 实例的环境下调用 Superfn 函数,就会在实例环境上执行 Superfn() 所有定义值对象或方法的初始化代码。这样 Subfn 每个实例就会具有自己的引用类型值副本。像上面这个例子,其中一个实例向 colors 中 push 一个值,另一个实例不会受到影响。

function Superfn(){
     this.colors = ["red", "blue", "green"];
}
function Subfn(){
    //继承了Superfn
    Superfn.call(this);
}

组合继承

构造函数 + 原型链的组合继承方式,是为解决各自本身的问题而组合使用,如下:

借用构造函数

  • 实现实例属性与方法继承
  • 解决了向被继承函数传值 与 原型共用引用类型值的问题

原型链

  • 实现原型属性与方法的继承
  • 解决了函数复用问题(方法在构造函数中定义,无法复用)
function Superfn(name){
     this.name = name;
     this.arr = [1,2,3]
}
Superfn.prototype.sayname = function(){
     console.log('mynameis'+this.name)
}

function Subfn(name,age){
     Superfn.call(this,name)
     this.age = age;
}
Subfn.prototype = new Superfn();
Subfn.prototype.constructor = Subfn;
Subfn.prototype.sayage = function(){
     console.log(this.age)
}

原型式继承

原型式继承是借助原型可以基于已有的对象创建新对象,没有必要创建继承函数的情况。只想让一个对象与另一个对象保持类似。

function object(o){
    function F(){}
    F.prototype = o;
    return new F();
}

一个 Demo 如下,基础信息一致的对象 info,需要在 info 基础上添加不同的变量。

var info = {
    year : '1989',
    from : 'china'
}

var example1 = object(info);
example1.name = 'saku';

var example2 = object(info);
example2.name = 'lau';

输出 example1 与 example2,发现原型中存在 info 的属性,并且包含各自的属性值。本质上是通过将 info 信息赋给了一个新的原型,所以如果基础对象info中存在引用类型值的话,多个 example 对象是会被共享引用类型值的。

寄生式继承

寄生式是原型式的一种扩展,创建一个用于封装继承的构造函数,在函数在内部以所需的方式增强对象。
例如原型式的这个例子,一次次的为实例创建新属性 name 步骤重复,如需要有更多操作就会显得代码臃肿,所以用一个函数 createInfo 将这些操作包装起来。

function object(o){
    function F(){}
    F.prototype = o;
    return new F();
}

function createInfo(original,name){
    var o = object(original);
    o.name = name;
    o.sayName = function(){
        console.log(this.name)
    }
    return o;
}

var info = {
    year : '1989',
    from : 'china'
}

var example1 = createInfo(info,'saku');
var example2 = createInfo(info,'lau');

主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。

寄生组合式继承

还记得组合继承吗?通过借用构造函数与原型链继承实现 superfn 实例属性与原型属性的继承。

function superfn(name){
    this.name = name;
    this.colors = ['red','white'];
}
superfn.prototype.sayName = function(){
    alert(this.name);
}
function supfn(name,age){
    superfn.call(this,name);//第二次调用superfn()
    this.age = age;
}
supfn.prototype = new superfn(); //第一次调用superfn()
supfn.prototype.sayAge = function(){
    alert(this.age);
}

虽然说组合继承是最常用的继承方式,但这种继承方式会存在着一个问题是两次调用了被继承 superfn 函数,例子代码已经清晰的注释调用位置与顺序,下面详细说明。

  1. 第一次调用,supfn的原型就会得到 superfn 的两个实例属性 name 和 colors。supfn 原型的 constructor 被覆盖为指向 superfn 原型的指针。
  2. 第二次调用,在 supfn 函数环境调用了 superfn,这时supfn的实例属性增加了 name 与 colors。于是 supfn 实例属性就屏蔽了 supfn 原型中得两个属性。supfn 的实例调用 name 与 colors 实际上是 supfn 的实例属性而不是原型属性。

所以需要寄生组合式继承来解决这个函数被多次调用的问题。以下利用寄生组合式继承修改后完整的代码。

function object(o){
    function f(){};
    f.prototype = o;
    return new f();
}

function inheritPrtotype(supf,superf){
    var prototype = object(superf.prototype);
    prototype.constructor = supf;
    supf.prototype = prototype;
}

function superfn(name){
    this.name = name;
    this.colors = ['red','white'];
}
superfn.prototype.sayName = function(){
    alert(this.name);
}
function supfn(name,age){
    superfn.call(this,name);
    this.age = age;
}
inheritPrtotype(supfn,superfn);
supfn.prototype.sayAge = function(){
    alert(this.age);
}

var example1 = new supfn('sakuya',14);
example1.colors.push('black');
example1.sayName(); //sakuya
example1.sayAge();  //14
console.log(example1.colors  //["red", "white", "black"]

var example2 = new supfn('lau',16);
example2.colors.push('orange');
example2.sayName();  //lau
example2.sayAge();   //16
console.log(example2.colors)   //["red", "white", "origin"]

首先supfn.prototype = new superfn()原型链继承语句替换成了 inheritPrototype 方法。原来的代码是将 superfn 的实例属性与原型属性赋给 supfn 的原型,这个方法所要做是只将 superfn 原型属性赋给 supfn 的原型,而不是一同将实例属性也赋给了 supfn 的原型,这样就解决组合继承多次调用 superfn 的问题。

再来看 inheritPrototype 这个函数,其中还调用了原型式继承的方法:

function object(o){
    function f(){};
    f.prototype = o;
    return new f();
}

function inheritPrtotype(supf,superf){
    var prototype = object(superf.prototype);
    prototype.constructor = supf;
    supf.prototype = prototype;
}

将 supfn 与 superfn 传到函数后执行步骤:

  1. 将 superfn 的原型传入object函数并返回了一个新对象赋值给了一个副本 prototype 变量
  2. 重新将副本的 constructor 指向 supfn ,修正副本的原型指向(在 object 里创建的新对象同时也会创建一个新的原型,而这个原型指向不是 supfn )
  3. 将修正 constructor 指针的副本对象赋给 supfn 的原型
  4. supfn 原型成功继承了 superfn 的原型属性,并没有 superfn 的实例属性。

最后附上一张其他例子的关系图(来自mqyqingfeng blog):

function Person() {}

Person.prototype.name = 'Kevin';

var person = new Person();

person.name = 'Daisy';
console.log(person.name) // Daisy

delete person.name;
console.log(person.name) // Kevin

ES6 Class

第一次接触 Class 语法的时候,就被这个基于原型继承的语法糖便利之处给吸引到。我非常推荐用 Class 来封装函数但前提是对原本继承方法有基本的了解。这里不对 Class 基本使用写太多,可在这里阅读ECMAScript 6 入门 Class篇。只写写需要注意的点。首先是静态方法,在 Class 中如果在函数前加 static 那么表示该方法不会被实例继承但可被子类继承调用,而是直接通过类来调用。静态方法包含的 this 指向类而不是实例。关于 Class 中的 prototype 属性和 proto 属性,建议阅读链接中文章。

class MathHelper {
  static sum(...numbers) {
    return numbers.reduce((a, b) => a + b)
  }
}
console.log(MathHelper.sum(1, 2, 3, 4, 5)) // <- 15

继承

Class 继承方式则需要用到 extends 关键字。在下面的例子代码中,constructor 是这个类的构造函数。this 代表实例对象与 function 方式写构造函数一致。在子类的构造函数中出现了 super 这个关键字。super 可当作函数或者对象使用。

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    toString() {
        return `X:${this.x}, Y:${this.y}`;
    }
}

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

  toString() {
    return this.color + ' ' + super.toString();
  }
}

Object.getPrototypeOf(ColorPoint) === Point // <- true

super 作为函数调用时代表父类的构造函数。子类 constructor 构造函数必须执行一次 super 函数。值得注意的是,super 虽然代表了父类 Point 的构造函数,但返回的是子类 ColorPoint 的实例,即 super 内部的 this 指的是 ColorPoint,因此 super 在这里相当于 Point.prototype.constructor.call(this)super 作为函数时只能用在子类的构造函数之中调用。

super 作为对象时,在普通方法中指向父类的原型对象,既可直接调用父类方法例如 super.toString() 相当于 Point.prototype.toString(),由于 Super 指向父类原型所以在父类构造函数中定义的方法属性无法调用。如果在 static 静态方法中 super 作为对象调用则指向父类而不是父类原型。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant