资讯专栏INFORMATION COLUMN

继承与原型

My_Oh_My / 332人阅读

摘要:既然构造函数有属于自己的原型对象,那么我们应该能让另一个构造函数来继承他的原型对象咯我们在构造函数内部执行了函数并改变了函数内部的指向其实这个指向的是实例化之后的对象。

我们在讨(mian)论(shi)JavaScript这门语言时,总是绕不过的一个话题就是“继承与原型链”。那么“继承与原型链”到底是什么呢?

我很喜欢的一个聊天模式是:我不能说XX是什么,我只能说XX像什么。也就是说我不直接跟你说定义,因为通常而言,“定义”所描述的概念很晦涩,比如关于“闭包”的定义——闭包是函数和声明该函数的词法环境的组合。

所以,我们先来看一下,JavaScript里到底“继承与原型链”是如何表现的。

“继承与原型链”像什么

不同于Java等的静态语言,在JavaScript这门语言里,我们没有“类”这个概念,所有的继承都是基于原型的。我们先直接看个例子:

var obj = {
    a: 0,
    f: function() {
        return this.a + 1
    }
}

var obj_1 = {} // 我们期望cat也能有sound属性跟speak方法

obj_1.__proto__ = obj

console.log(obj_1.a) // 0
console.log(obj_1.f()) // 1

如上,我们定义obj_1这个对象的时候,并没有声明a属性跟f方法,但是我们依然可以找到它们。这是因为在JavaScript中,你在一个对象上寻找某个属性(JavaScript对象都是键值对的形式,所以方法其实也可以算一个属性),他首先会在该对象本地寻找,如果没有,他会顺着原型链一层层往上寻找。

在上面的栗子中,对象obj_1本地没有定义任何属性,所以当我们执行obj_1.a的时候,会顺着原型链往上找。在obj_1.__proto__ = obj这句里,我们将obj赋值给了obj_1__proto__属性。

但是等等,__proto__是什么?

__proto__属性指向的就是obj_1的原型,obj的原型是什么呢?我们可以打印obj.__proto__来看看,结果打印出来一大堆东西,这些其实就是Object.prototype,也就是“终极原型”,这个对象不再继承任何原型。按照之前说的,obj_1应该也能直接访问到这上面的属性。事实也的确如此,比如:

obj_1.hasOwnProperty("a") // false

我们并没有在obj_1上定义hasOwnProperty方法,但是依然可以找到该方法。事实上,所有以对象字面量(Object Literal)形式创建出来的对象,都继承了有Object.prototype上的所有属性。

那么我们能不能创建一个不继承自任何原型的对象呢?答案是可以的。

JavaScript为我们提供了一个方法叫Object.create,通过它,我们可以创建一个原型为特定对象的对象。如果我们传入一个null,那么我们就能创建一个“原型为空”的对象。

var a = Object.create(null)

在这个例子里,a成了一个空的对象,不仅本地没有任何属性,连原型链都没有,也就是说它甚至都没有继承Object.prototype。(思考:这样的空对象到底有什么作用呢?)

这样一来,我们也可以利用Object.create来实现继承咯?对的。

var obj = {
    a: 0,
    f: function() {
        return this.a + 1
    }
}

var obj_2 = Object.create(obj)
console.log(obj_2.a) // 0
console.log(obj_2.f()) // 1

但是重新想象,继承的本质是什么?继承原型!那么不管用什么方法,只要在我的原型链上能找到你就行了。

现在有一个问题,obj上定义了一个属性a,如果我在obj_2上再定义一个属性a,那么打印出来的会是谁的a呢?

var obj = {
    a: 0,
    f: function() {
        return this.a + 1
    }
}

var obj_2 = Object.create(obj)
obj_2.a = 2
console.log(obj_2.a) // 2

答案是显而易见的,因为我们在寻找一个属性的时候,总是从当前对象本地开始的,如果在当前对象上找到了这个属性,那么查询就停止了。所以,如果原型链过长,在查找一个靠前的原型上的属性的时候,就会比较耗时。我们应当尽量避免这种过长的原型链。

“继承与原型链”是什么

读到这里,相信我们已经能够对继承原型链做一个定义了。

原型链

原型链就是从一个对象的__proto__开始,一直到这条线的最末端,大部分情况下,这个最末端就是Object.prototype。例如上面的那个例子:

var obj = {
    a: 0,
    f: function() {
        return this.a + 1
    }
}

var obj_2 = Object.create(obj)

// obj_2.__proto__ === obj
// obj.__proto__ === Object.prototype
继承

在这个例子里,obj --- Object.prototype就组成了一个原型链,顺着原型链,我们可以找到这个对象最开始继承自哪个对象,同时,原型链上的每一个节点都可以继承上游对象的所有属性。继承描述的应该是一种关系,或者一种动作。

new运算符

在前面的篇幅里我们知道,在JavaScript里,对象可以用字面量的形式与Object.create的形式来创建。但是JavaScript里还有一种方式来创建一个对象,那就是使用new运算符。

var obj = new Object

console.log(obj) // {}

根据前面的内容,我们可知obj继承了Object.prototype对象上的属性。关于new操作符,可以看我的另一篇专栏当我们在JavaScript中new一个对象的时候,我们到底在做什么。那么Object是什么?

我们来执行一下typeof Object,打印出来的是"function"。对的,Object是一个函数,准确地说,它是一个构造函数。new运算符操作的,应该是一个函数。

我们可以对任意函数执行new操作。但是一个函数如果被用作了构造函数来实例化对象,那我们倾向于把它的首字母大写。

var Foo = function(x) {
    this.x = x
}

var boo = new Foo(1)
console.log(boo, boo.x) // Foo {x: 1} 1

构造函数能让我们初始化一个对象,在构造函数里,我们可以做一些初始化的操作。通常我们在编写一些JavaScript插件的时候会在全局对象上挂载一个构造函数,通过实例化这个构造函数,我们可以继承它的原型对象上的所有属性。

既然构造函数有属于自己的原型对象,那么我们应该能让另一个构造函数来继承他的原型对象咯?

var Human = function(name) {
    this.name = name
}
var Male = function(name) {
    Human.call(this, name)
    this.gender = "male"
}

var jack = new Male("jack")
console.log(jack) // Male {name: "jack", gender: "male"}

我们在构造函数内部执行了Human函数并改变了Human函数内部的this指向(其实这个this指向的是实例化之后的对象)。同时,我们在Male的原型上定义一个自己的属性gender,这样,实例化出来的对象同时有了两个属性。

但是这个继承完整么?继承是需要继承原型的,但是jack的原型链上并没有Human,我们需要额外两步。

var Human = function(name) {
    this.name = name
}
var Male = function(name) {
    Human.call(this, name)
    this.gender = "male"
}

Male.prototype = Object.create(Human.prototype)
Male.prototype.constructor = Male

var jack = new Male("jack")
console.log(jack) // Male {name: "jack", gender: "male"}

这样一来,我们就能在jack的原型链上找到Human了。

ES6的类

其实前面一节看起来会比较晦涩,因为在ES6之前,JavaScript没有类的概念(当然之后也没有),但是我们却有“构造函数”,那上面一节的栗子就应该说是构造函数Male继承了构造函数Human

我记得当时场面有点尴尬,大家都搓着手低着头都不知道说点儿什么

好在ES6里我们有了Class的关键字,这是个语法糖,本质上,JavaScript的继承还是基于原型的。但是,至少形式上,我们可以按照“类”的方式来写代码了。

class Human {
    constructor(name) {
        this.name = name
    }
}
class Male extends Human {
    constructor(name) {
        super(name)
        this.gender = "male"
    }
}

var jack = new Male("jack")
console.log(jack) // Male {name: "jack", gender: "male"}

在控制台上顺着__proto__一层层往下翻,我们会能找到class Maleclass Human,这说明我们的继承成功了。同时,我们也可以理解成“类Male继承了类Human”,虽然在JavaScript其实并没有类这个东西。

结语

其实通篇的核心还是那句话:JavaScript的继承是基于原型的。很多内容我没有展开讲解很多,表达了主干即可。

引用

继承与原型

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

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

相关文章

  • JavaScript系列--浅析原型继承

    摘要:综上所述有原型链继承,构造函数继承经典继承,组合继承,寄生继承,寄生组合继承五种方法,寄生组合式继承,集寄生式继承和组合继承的优点于一身是实现基于类型继承的最有效方法。 一、前言 继承是面向对象(OOP)语言中的一个最为人津津乐道的概念。许多面对对象(OOP)语言都支持两种继承方式::接口继承 和 实现继承 。 接口继承只继承方法签名,而实现继承则继承实际的方法。由于js中方法没有签名...

    draveness 评论0 收藏0
  • 创建对象(一):创建继承

    摘要:创建实例的方式有三种对象字面量表示法操作符跟构造函数中的函数。下面主要讲的是最为复杂的操作符跟构造函数的创建对象实例的方法。 创建对象 一.创建对象的方法 理解原型对象: 无论什么时候,只要创建了新函数,就会根据一组特定的规则为该函数创建一个 prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor属性,这个属性包含一个指向p...

    microelec 评论0 收藏0
  • JavaScript 的继承方式及优缺点

    摘要:继承简介在的中的面向对象编程,继承是给构造函数之间建立关系非常重要的方式,根据原型链的特点,其实继承就是更改原本默认的原型链,形成新的原型链的过程。 showImg(https://segmentfault.com/img/remote/1460000018998684); 阅读原文 前言 JavaScript 原本不是纯粹的 OOP 语言,因为在 ES5 规范中没有类的概念,在 ...

    nanchen2251 评论0 收藏0
  • JS学习笔记(第6章)(面向对象之继承——JS继承的六大方式)

    摘要:除此之外,在超类型的原型中定义的方法,对子类型而言也是不可兼得,结果所有类型都只能用构造函数模式。创建对象增强对象指定对象继承属性这个例子的高效率体现在它只调用了一次构造函数。 1、原型链 原型链的基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。构造函数、原型和实例的关系:每个构造函数都有一个原型对象;原型对象都包含着一个指向构造函数的指针;实例都包含一个指向原型对象的...

    lscho 评论0 收藏0
  • 前端进击的巨人(七):走进面向对象,原型原型链,继承方式

    摘要:除了以上介绍的几种对象创建方式,此外还有寄生构造函数模式稳妥构造函数模式。 showImg(https://segmentfault.com/img/remote/1460000018196128); 面向对象 是以 对象 为中心的编程思想,它的思维方式是构造。 面向对象 编程的三大特点:封装、继承、多态: 封装:属性方法的抽象 继承:一个类继承(复制)另一个类的属性/方法 多态:方...

    wums 评论0 收藏0

发表评论

0条评论

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