资讯专栏INFORMATION COLUMN

独家解析Javascript原型继承

verano / 3217人阅读

摘要:面向对象实现代码动物发声汪汪喵喵调用代码动物发声喵喵动物发声汪汪当要增加一种动物时,只需增加一个继承,不会影响其他已有的动物逻辑。所以的继承和的原型继承,可谓殊途同归。

传统面向对象的继承和多态

我们知道C++/Java/C#等面向对象语言,都原生地支持类的继承。继承的核心作用大抵是创建一个派生类,并使其复用基本类(即父类)的字段和/或方法。并且派生类可以重写基本类的方法。这样基本类和派生类相同签名的方法在被调用时,就会有不同的行为表现,即为多态的实质。换句话说,多态是透过继承重写实现的。

举例:实现不同的动物叫声不同。
过程式编程(Java代码):

void animalSpeak(String animal) {
    if(animal == "Dog") {
        System.out.println("汪汪");
    } else if(animal == "Cat") {
        System.out.println("喵喵");
    } else {
      System.out.println("动物发声");
    }
}

//调用代码
animalSpeak("Dog"); //汪汪
animalSpeak("Cat"); //喵喵
animalSpeak("");    //动物发声

这里一个问题是,如果增加一种动物,就要在speak方法增加if分支,此方法逐渐变得臃肿难维护。偶尔因增加一种动物,会偶尔误伤其他动物的逻辑,也未尝可知。面向对象式的动态应运而生。

面向对象实现(java代码)

class Animal {
    void speak() {
       System.out.println("动物发声:");
    }
}

class Dog extends Animal {
    void speak() {
       super.speak();
       System.out.println("汪汪");
    }
}

class Cat extends Animal {
    void speak() {
       super.speak();
       System.out.println("喵喵");
    }
}

void animalSpeak(Animal animal) {
    animal.speak();
}

//调用代码
animalSpeak(new Cat());     //动物发声: 
                            //喵喵
animalSpeak(new Dog());     //动物发声: 
                            //汪汪

当要增加一种动物时,只需增加一个class继承 Animal,不会影响其他已有的动物speak逻辑。可看出,面向对象多态编程的一个核心思想是便于扩展维护

结语:面向对象编程以继承产生派生类和重写派生类的方法,实现多态编程。核心思想是便于扩展和维护代码,也避免if-else

JavaScript继承实现

Java继承是class的继承,而JavaScript的继承一般是通过原型(prototype)实现。prototype的本质是一个Object实例,它是在同一类型的多个实例之间共享的,它里面包含的是需要共享的方法(也可以有字段)。
JavaScript版原型继承的实现:

function Animal() {
}
Animal.prototype.speak = function () {
    console.log("动物发声:");
}

function Dog(name) {
    this.name = name;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.speak = function () {
    //通过原型链找‘基本类’原型里的同名方法
    this.__proto__.__proto__.speak.call(this);
    console.log("汪汪, 我是", this.name);
}

function Cat(name) {
    this.name = name;
}
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;
Cat.prototype.speak = function () {
    //通过原型链找‘基本类’原型里的同名方法
    this.__proto__.__proto__.speak.call(this);
    console.log("喵喵, 我是", this.name);
}

//调用代码
function animalSpeak(animal) {
    animal.speak();
}

animalSpeak(new Dog("大黄"))
console.log()
animalSpeak(new Cat("小喵"))

//动物发声:
//汪汪, 我是 大黄

//动物发声:
//喵喵, 我是 小喵
JavaScript原型剖析

传统面向对象语言的class继承是为代码(方法和字段)复用,而JavaScript的prototype是在同类型实例之间共享的对象,它包含共享的方法(也可有字段)。所以java的class继承和javascript的原型继承,可谓殊途同归。为了方便理解js原型,提出两个概念:原型的design-time和run-time

Design-time 原型

可理解为我们(程序员)如何设计js类型的自上而下的继承关系。以上例看出,design-time是通过prototype赋值实现。

// Dog类型继承自Animal
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Run-time 原型和原型链

Run-time可理解为,原型设计好之后,要创建实例了, 并且还能向上查找其继承自哪个原型

// 创建Dog类型的一个实例
var dog = new Dog("大黄");

// ***打起精神*** 这里到了关键的地方:如何查找dog创建自哪个类型:
// 显而易见, dog是由Dog创造出来的
dog.__proto__ // Dog { constructor: [Function: Dog], speak: [Function] }

// 再往上查找原型链:
// 看出来是继承了Animal的原型
dog.__proto__.__proto__  //Animal { speak: [Function] }

// 再往上查找原型链:
// 看出来是继承了Object的原型
dog.__proto__.__proto__.__proto__ // {}

// 再往上查找原型链:
// 到了原型链的顶端。
dog.__proto__.__proto__.__proto__.__proto__ // null

所以在调用实例的方法,它会在原型链上自下而上,直到找到该方法。如果到了原型链顶端还没有找到,就抛错了。

结语: design-time原型是通过prototype赋值,设计好自上而下的继承关系; run-time时通过实例的__proto__,自下而上在原型链中查找需要的方法。

再论prototype 与__proto__

如前文述prototype可看成design-time的概念,以此prototype创造出一个实例。

var dog = new Dog("大黄");
//实质是:
//new 是便利构造方法
var dog = Object.create(Dog.prototype);
dog.name = "大黄"

__proto__是属于实例的,可反查实例出自哪个prototype。 所以dog.__proto__显然等于Dog.prototype.

  dog.__proto__ == Dog.prototype //true

而Dog.prototype创自于 Animal.prototype:

Dog.prototype = Object.create(Animal.prototype);

所以Dog.prototype的__proto__即为Animal.prototype

 Dog.prototype.__proto__ == Animal.prototype //true
 dog.__proto__.__proto__ == Animal.prototype //true

这样就实现了run-time原型链自下而上的查找

结束语

原型继承是JS老生常谈的话题,也是很重要但不易深入理解的技术点。本文里提出了design-time原型设计和run-time原型链查找,希望有助于此技术点的理解。

后记

上文是对自定义函数类型实例的原型分析,漏掉了对JS内置类型的原型分析。大家知道JS内置类型是有:

undefined
null
bool
number
string
object
    Function
    Date
    Error
 symbol (ES6)
JS基本(primitive)类型原型分析

基本类型有undefined, null, bool, number, string和symbol. 基本类型是以字面量赋值函数式赋值的,而非通过new或Object.create(...)出来。
基本类型是没有design-time的prototype。但undefined/null之外的基本练习,还是有run-time的__proto__

// 常用基本类型的字面量赋值:
var b = true;
var n = 1;
var s = "str";

// 常用基本类型的run-time __proto__
// 需要说明的是,基本类型本身是没有任何方法和字段的。
// 例如undefined/null, 不能调用其任何方法和字段。
// 这里调用b.__proto__时,会临时生成一个基本类型包装类的实例,
// 即生成var tmp = new Boolean(b)。这是个object实例,返回__proto__
b.__proto__ // [Boolean: false]
n.__proto__ // [Number: 0]
s.__proto__ // [String: ""]
// 以上 *.__proto__ 打印出的是,字面量值出自哪个函数, 所以亦可以函数方式赋值, 跟字面量完全等价。

// 基本类型的函数式赋值:
// *注意*的是,这里仅是调用函数,没有new。若用了new, 就构造出一个对象实例,而再非基本类型了
b = Boolean(true)
n = Number(1)
s = String("")
sym = Symbol("IBM") // ES6 新增的symbol没有字面量赋值方式

// 特殊的case是undefined和null, 只有字面量赋值(没函数方式赋值)
// null 和 undefined是没有__proto__的。
// 也可以理解为,null处于任何原型链的最顶端,这是因为null是object类型(typeof null == "object")。undefined不是object类型。
var nn = null;
var un = undefined;
JS基本类型的对象实例的原型分析

如果通过new 构造基本类型的对象实例,那么就是对象而非原生态基本类型了。

var b = new Boolean(true)
var n = new Number(1)
var s = new String("")

它们就遵循自定义函数类型对象的原型法则了。以上述Number n为例:

// Number的 design-time的prototype:
Number.prototype //[Number: 0]
typeof Number.prototype //"object"
Number.prototype instanceof Object // true. 原型本身就是一对象实例

// n的run-time __proto__原型链
n.__proto__ //[Number: 0],n是由Number函数构造产生的
// 可看出,n 继承了object类型
n.__proto__.__proto__ // {}
// n的原型链顶端也是null
n.__proto__.__proto__.__proto__ // null
JS内置object类型的原型分析

JS内置的Date, Error, Function其本身就是function,就是说 typeof Date, typeof Error, typeof Function 都是 ‘function". 所以读者可用本文分析自定义函数类型原型的方法,自行分析这三者的design-time的prototype, 以及run-time的__proto__原型链。

需要特殊指出的是,我们几乎不会用new Function的方式去创建Function的实例,而是透过function关键字去定义函数。

// 一般是用function这个关键字,去定义函数。这和通过new Function构造本质是一样的
function foo() { }

// 通过run-time的 __proto__,其实可看出,foo就是Function这个类型的一个实例
foo.__proto__ //[Function]
foo instanceof Foo // true

// 所以foo也就继承了Function的design-time prototype
// 而理解这一点很重要。
add.__proto__ == Function.prototype // true

// 函数实例本身也是object类型
foo.__proto__.__proto__ //{}
foo instanceof Object // true
{}和Object.create(null)的区别

以下定义是等价的

var obj = {} // 字面量
var obj = new Object() // Object函数构造
var obj = Object.create(Object.prototype) // Object原型构造

obj run-time的__proto__即Object.prototype, 故obj继承了Object.prototype的共享方法,例如toString(), valueOf()

obj.__proto__ == Object.prototype //true
obj.toString()                   //"[object Object]"
var obj2 = Object.create(null)
obj2.__proto__  // undefined
obj2.toString() //TypeError: obj2.toString is not a function

可以看出,obj2无run-time的__proto__,没有继承Object.prototype,故而就不能调用.toString()方法了

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/84016.html

相关文章

  • 独家解析Javascript原型继承 - 之函数原型和AOP

    摘要:引子独家解析原型继承已经比较全面的分析了自定义函数类型,内置基本类和内置对象类型的的以及的原型链。鉴于函数是的一等公民,另辟新篇介绍函数的原型及其应用。函数本身也是对象,它遵循独家解析原型继承所描述的自定义函数类型对象的原型法则。 引子 独家解析Javascript原型继承已经比较全面的分析了自定义函数类型,JS内置基本类(undefined, null, bool, number, ...

    ispring 评论0 收藏0
  • 闲话JavaScript数据类型

    摘要:支持的类型的内置数据类型罗列如下自定义自定义这三种类型的赋值是同类似的。这根不同,这因为是没有包装类新增的基本类型,只支持函数式赋值,不支持字面量和函数构造。 JavaScript支持的类型 JS的内置数据类型罗列如下: undefined null bool number string function object Function Date ...

    jerryloveemily 评论0 收藏0
  • 2017-07-16 前端日报

    摘要:前端日报点赞通道精选听说你没来腾讯前端求职直播课笔试篇淘宝漏洞修补记一次踩坑记录中的对象精读发布中文深入理解笔记块级作用域绑定架构经验分享深入理解笔记字符串和正则表达式架构经验分享深入理解笔记导读架构经验分享第期种使用提升应 2017-07-16 前端日报 GitHub点赞通道 精选 听说你没来 JSConf 2017腾讯前端求职直播课——笔试篇淘宝 flexible.js 漏洞修补:...

    yeyan1996 评论0 收藏0
  • javascript继承 --- 多种继承方式解析(ES5)

    摘要:继承前言作为一门轻量级的脚本语言在和的横空出世之后将其推向的新的高度虽然中出现的新的生成对象的类语法格式但依然为的语法糖而我们依然有必要从的原生实现入手来了解它的继承实现方式给出了更加简洁的固定的类声明方式有兴趣的可以查看阮一峰的入门下面给 javascript继承 前言 javascript作为一门轻量级的脚本语言在ES6和node.js的横空出世之后将其推向的新的高度,虽然 ES6...

    yankeys 评论0 收藏0
  • JavaScript常用6大继承方式解析

    摘要:特点跟借用构造函数模式一样,每次创建对象都会创建一遍方法。缺点寄生组合式继承使用时说明解决了组合继承存在的问题特点只调用了一次构造函数,并且因此避免了在上面创建不必要的多余的属性原型链还能保持不变还能够正常使用和缺点参考资料 原型链继承 //父类 function Person(name, age) { this.name = name; this.age = age; ...

    yearsj 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<