对象、原型链、类、继承【上】

概述

JavaScript,或者说ECMAScript 具备面向对象语言的一些特色,但它不是一门纯粹的面向对象语言,由于它也包含着函数式编程的一些东西。事实上,如今不少的面向对象的语言,好比Java,也开始实现一些函数式的新特性。总之,全部的编程语言都在随着应用场景的变化而不断进化。编程

这篇文章尽量的将ECMAScript这门语言中关于面向对象的实现表述彻底。好了,咱们先从对象开始吧!数组

对象

对象的定义:无序属性的集合,其属性能够包含基本值、对象、或者函数。能够看作一个散列。浏览器

对象的建立:每一个对象都是基于一个引用类型建立的,这个引用类型能够是原生类型,也能够是自定义的类型。安全

对象的属性类型

对象有属性(property),属性有特性(attribute),特性通常表示形式为双方括号(如[[attribute]])。编程语言

对象有两种属性(property):数据属性访问器属性函数式编程

类型:数据属性

数据属性包含一个数据值的位置。在这个位置能够读/写值。数据属性有四个特性([[attribute]]):函数

  • [[Configurable]] 表示可否经过delete删除属性,可否修改属性的特性值,可否把属性修改成访问器属性。直接在对象上定义的属性的默认值为true
  • [[Enumerable]] 表示可否经过for-in循环返回属性。直接在对象上定义的属性的默认值为true
  • [[Writable]] 表示可否修改属性的值。直接在对象上定义的属性的默认值为true
  • [[Value]] 属性的数据值。此处用来存储。默认值为undefined

defineProperty方法能够配置属性的特性。(IE9+)post

var obj = {}
Object.defineProperty(obj, 'x', {
    configurable: true,
    enumerable: true,
    writable: true,
    value: 123
})
obj; // {x: 123}
复制代码

类型:访问器属性

访问器属性没有数据值([[Value]]),因此也没有([[Writable]]),可是多了[[Get]][[Set]]。也叫gettersetter。用于读、写对应属性的值。测试

  • [[Configurable]] 表示可否经过delete删除属性,可否修改属性的特性值,可否把属性修改成访问器属性。直接在对象上定义的属性的默认值为true
  • [[Enumerable]] 表示可否经过for-in循环返回属性。直接在对象上定义的属性的默认值为true
  • [[Get]] 读取属性时调用的函数。默认值是undefined
  • [[Set]] 写入属性时调用的函数,参数为写入值,默认值是undefined

访问器属性不能直接定义,必须使用defineProperty来定义。ui

var book = {
    _page: 2
}
Object.defineProperty(book, 'page', {
    get: function () {
        console.log('你调用了get方法')
        return this._page
    },
    set: function (val) {
    console.log('你调用了set方法')
        this._page = val
    }
})
book.page; // 你调用了get方法
book.page = 3 // 你调用了set方法
复制代码

defineProperty是ES5新加的方法,在此以前,对于gettersetter,浏览器内部有本身的实现。

var book = {
    _page: 2
}
book.__defineGetter__('page', function () {
    return this._page
})
book.__defineSetter__('page', function (val) {
    this._page = val
})
复制代码

定义多个属性

defineProperties()(ES5,IE9+)能够同时定义多个属性。

var book = {}
Object.defineProperties(book, {
    _page: {
        value: 2
    },
    author: {
        value: 'JiaHeSheng'
    },
    page: {
        get: function () {
            return this._page
        },
        set: function (val) {
            this._page = val
        }
    }
})
book;
/* { author: "JiaHeSheng" page: 2 _page: 2 get page: ƒ () set page: ƒ (val) } */
复制代码

读取属性的特性

如何查看对象某个属性的特性呢?ES5提供了getOwnPropertyDescriptor方法, 它返回一个属性所拥有的特性组成的对象。

var obj = { x: 456 }
Object.getOwnPropertyDescriptor(obj, 'x')
/* { configurable: true enumerable: true value: 456 writable: true } */
复制代码

建立对象

建立对象最简单的模式就是经过对象字面量进行建立,如var obj = {}。也能够经过Object构造函数,配合new命令进行建立,如var obj = new Object()。但这些都适用于建立单个对象,若是我要批量建立一些「具备某些相同属性」的对象呢?

工厂模式

经过参数传入工厂函数,每次均可已生成一个包含特有信息和相同信息的新对象。但缺点是,咱们并不能经过其生成的实例找到与对应的工厂函数的关联。

function factoryFoo (name, age) {
    var o = {}
    o.name = name;
    o.age = age;
    o.say = function () {
        console.log(this.name)
    }
    return o
}
var p1 = factoryFoo('Tom', 23)
var p2 = factoryFoo('Jack', 24)
p1.constructor // Object
p1 instanceof factoryFoo // false
复制代码

咱们能够看到,实例的constructor指向了Object,而且也不能证实p1是工厂函数的实例。

构造函数模式

为了解决工厂函数带来了问题,咱们试着使用构造函数+new来生成实例。

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

var p1 = new Person('Tom', 23)
var p2 = new Person('Jack', 24)

p1.constructor // Person
p2.constructor // Person

p1 instanceof Person // true
p1 instanceof Object // true
复制代码

咱们能够看到,使用构造函数模式构造出来的对象实例,能够经过其constructor属性找到它的构造函数。(解决了工厂函数的问题)

另外,与工厂函数相比,少了显式地建立对象,少了return语句。这是由于使用new操做符,隐式地作了这些事情。

什么是构造函数

构造函数与普通函数的区别就是,它被new命令用来建立了实例。换言之,没有被new操做的构造函数就是普通函数。

构造函数的缺点

构造函数也有它的缺点:每一个方法都会在实例化的时候被从新创造一遍,即便它们如出一辙。 上例中的say方法就被创造了两次。

// 建立实例时
this.say = new Function('console.log(this.name)')

// 建立后
p1.say === p2.say // false
复制代码

为了解决这个问题,咱们能够这样:

function say () {
    console.log(this.name)
}
function Person (name, age) {
    this.name = name;
    this.age = age;
    this.say = say
}
复制代码

可是这又引出了一个新问题:总不能每一个方法都这样全局定义吧?

new 运算符

new运算符用来建立一个用户定义的对象类型的实例或具备构造函数的内置对象的实例。

当代码 new Foo(...) 执行时,会发生如下事情:

  1. 一个继承自 Foo.prototype 的新对象被建立。
  2. 使用指定的参数调用构造函数 Foo ,并将 this 绑定到新建立的对象。new Foo 等同于 new Foo(),也就是没有指定参数列表,Foo 不带任何参数调用的状况。
  3. 由构造函数返回的对象就是 new 表达式的结果。若是构造函数没有显式返回一个对象,则使用步骤1建立的对象。(通常状况下,构造函数不返回值,可是用户能够选择主动返回对象,来覆盖正常的对象建立步骤)

原型模式

为了解决上面的问题,ECMAScript语言中有了原型(prototype)和原型链的概念。

每个函数上都有一个prototype属性,这个属性是一个指针,指向一个对象,这个对象包含了一些属性和方法,这些属性和方法能够被全部由这个函数建立的实例所共享。

举例来讲,任意一个函数Personprototype属性指向对象prototypeObject对象,全部由new Person()建立的实例(p1p2...pn),都会共享prototypeObject的属性和方法。

function Person (){
}
Person.prototype.age = 34;
Person.prototype.getAge = function () {
    return this.age
}
var p1 = new Person()
var p2 = new Person()
p1.age === p2.age // true
p1.age // 34
p1.getAge === p2.getAge // true
p2.getAge() // 34
复制代码

构造函数、实例、原型对象

不管何时,建立一个新函数,新函数就会有prototype属性,它指向该函数的原型对象。 默认状况下,每一个原型对象都有一个属性constructor,它指向原型所在的函数。 当调用这个函数生成出一个实例以后,生成的实例有个隐藏的属性(不可见,也没法访问)[[prototype]],它指向原型对象。 幸亏,浏览器实现了这个属性:__proto__,经过这个属性能够访问原型对象。不过这不是标准实现,不建议在生产环境中使用。

经过上面的示例和描述,我制做了一张图片,说明「构造函数、实例、原型对象」的关系:

构造函数、实例、原型对象的关系

知道他们的关系以后,咱们看下经过哪些方法能够查看他们关系。

// 证实 p1是 Person 的实例
p1.constructor === Person // true
p1 instanceof Person // true

// 证实 Person.prototype 是 p1 的原型对象
Person.prototype === p1.__proto__ // true
Person.prototype.isPrototypeOf(p1) // true
Object.getPrototypeOf(p1) === Person.prototype // true
复制代码

点击查看Object.prototype.isPrototypeOf()Object.getPrototypeOf的详细用法。

实例、原型对象上的属性和方法

若是要读取实例上的属性或者方法,就会如今实例对象上搜索,若是有就返回搜到的值;若是没有,继续在实例的原型对象上搜索,若是搜到,就返回搜到的值,若是没搜到,就返回undefined。 下面是示例:

function Person () {}
var p1 = new Person();
p1.x // undefined

Person.prototype.x = 'hello'
p1.x // hello 来自原型对象

p1.x = 'world'
p1.x // world 来自实例
Person.prototype.x // hello
复制代码

这是抽象出来的搜索流程图:

寻找属性值

咱们在代码示例中看到,给示例的属性赋值,并无覆盖原型上对应的属性值,只是在搜索时,屏蔽掉了而已。 而这就是使用原型对象的好处:生成的实例,能够共享原型对象的属性和方法,也能够在自身自定义属性和方法,即便同名也互不影响,而且优先使用实例上的定义。

继续看下面的代码:

p1.x = null
p1.x // null 来自实例
delete p1.x
p1.x // hello 来自原型对象
复制代码

设置属性值为null,获取属性的时候,并不会跳过实例,若是要从新创建与原型对象的连接,可使用delete删除实例上的属性。

那么如何知道当前获取的属性值是在实例仍是在原型对象上面定义的呢?ECMAScript提供了hasOwnProperty方法,该方法会忽略掉那些从原型链上继承到的属性。

p1.x = 'world'
p1.hasOwnProperty('x') // true
delete p1.x
p1.hasOwnProperty('x') // false
Person.prototype.x = 'hello'
p1.hasOwnProperty('x') // false
Person.prototype.hasOwnProperty('x') // true
复制代码

原型与in操做符

in操做符会在经过对象可以访问给定属性时返回true,不管该属性存在于实例仍是原型中。

Person.prototype.x = 'hello'
'x' in Person.prototype // true
'x' in p1 // true
p1.x = 'world'
'x' in p1 // true
复制代码

组合使用in操做符和hasOwnProperty便可判断取到的属性值,是否是存于原型中的。

function hasPrototypeProperty (obj, name) {
    return !obj.hasOwnProperty(name) && (name in obj)
}
Person.prototype.x = 'hello'
hasPrototypeProperty(p1, 'x') // true
p1.x = 'world'
hasPrototypeProperty(p1, 'x') // false
复制代码

那么,如何获取对象上全部自身的属性和方法呢?

  • Object.keys。能够获取对象上全部自身的可枚举属性和方法名,返回一个名称列表。

  • Object.getOwnPropertyNames。能够获取对象上自身的全部属性和方法名,包括不可枚举的,也返回一个名称列表。

更简单的原型语法

上面示例中,咱们添加原型属性,是一个一个在Person.prototype上添加。为了减小没必要要的输入,视觉上也更易读,咱们能够把要添加的属性和方法,直接封装成对象,而后改变Person.prototype指向的位置。

function Person () {}
Person.prototype = {
    age: 34,
    getAge: function () {
        return this.age
    }
}
复制代码

可是,若是这样作,Person.prototype.constructor也被重写,指向了封装对象的构造函数,也就是Object

Person.prototype.constructor === Object // true
复制代码

这时,咱们已经没法经过constructor知道原型对象的构造类型了。若是你还记得,工厂模式也存在这个问题。咱们能够这样作:

function Person () {}
Person.prototype = {
    constructor: Person,
    age: 34,
    getAge: function () {
        return this.age
    }
}
复制代码

可是,这样也会有问题。默认的constructor是不可枚举的,这样显式的赋值以后,就会变成可枚举的了。

Person.hasOwnProperty('constructor') // true
复制代码

若是你很在乎这个,可使用defineProperty,修改constructor属性为不可枚举。

Object.defineProperty(Person.prototype, 'constructor', {
    enumerable: false,
    value: Person
})
复制代码

原型的动态性

由于联系原型对象和实例的只是一个指针,而不是一个原型对象的副本,因此原型对象上属性的任何修改都会在实例上反应出来,不管实例建立是在改动以前或者以后。

function Person () {}
Person.prototype.age = 12
var p1 = new Person()
p1.age // 12
Person.prototype.age = 24
p1.age // 24
var p2 = new Person()
p2.age // 24
复制代码

但若是修改了整个原型对象,那状况不同了。由于重写原型对象会切断构造函数与原先原型对象的联系,而实例的指针指向的倒是原来的原型对象。

function Person () {}
Person.prototype = {
    age: 12
}
var p1 = new Person()
p1.age // 12
p1.__proto__ === Person.prototype // true

Person.prototype = {
    age: 24
}
p1.age // 12
Person.prototype.age // 24 
p1.__proto__ === Person.prototype // false
复制代码

因此,修改原型对象是把双刃剑,用得好能够解决问题,用很差就会带来问题。

原生对象的原型

原生对象(ObjectArrayString等)其实也是构造函数

typeof Object // function
typeof Array // function
typeof String // function
复制代码

它们自身拥有一些属性和方法,它们的原型对象也拥有一些,而原型对象上面的属性和方法,都会被它们构造的实例所共享。

Object.getOwnPropertyNames(Object).join(',') // "length,name,prototype,assign,getOwnPropertyDescriptor,getOwnPropertyDescriptors,getOwnPropertyNames,getOwnPropertySymbols,is,preventExtensions,seal,create,defineProperties,defineProperty,freeze,getPrototypeOf,setPrototypeOf,isExtensible,isFrozen,isSealed,keys,entries,values"

Object.getOwnPropertyNames(Object.prototype).join(',') // "constructor,__defineGetter__,__defineSetter__,hasOwnProperty,__lookupGetter__,__lookupSetter__,isPrototypeOf,propertyIsEnumerable,toString,valueOf,__proto__,toLocaleString"

Object.getOwnPropertyNames(Object.getPrototypeOf({})).join(',') // "constructor,__defineGetter__,__defineSetter__,hasOwnProperty,__lookupGetter__,__lookupSetter__,isPrototypeOf,propertyIsEnumerable,toString,valueOf,__proto__,toLocaleString"
复制代码

既然能够共享,固然也能够修改和添加。

Object.prototype.toString = function () {
    return 'hello world'
}
var a = {}
a.toString() // hello world
复制代码

虽然这样很方便,可是,咱们并不推荐这么作。由于每一个原生对象的属性和方法,都是有规范可寻的,而且这个规范是全部开发人员都承认的。那么,若是「自定义」了这些属性和方法,可能在多人协做的项目中引发没必要要冲突。而且若是规范更新,也会带来问题。

原型对象的问题

上面说了使用原型对象的诸多优势,可是原型模式也是有问题的。原型模特的优势是由于它的共享特性,缺点也是。好比,咱们在原型对象上定义了一个引用类型的属性。

function Person () {}
Person.prototype.family = ['father','mother']
var p1 = new Person()
var p2 = new Person()
p1.family.push('girlFriend')

p1.family // ["father", "mother", "girlFriend"]
p2.family // ["father", "mother", "girlFriend"]
复制代码

咱们在p1family属性中添加了girlFriend,可是p2.family也添加了,由于他们指向的是同一个数组。而这,是咱们不但愿看到的。实例之间须要共享的属性和方法,天然,也须要自有的属性和方法。

实例属性(OwnProperty) 该属性在实例上,而不是原型上。能够在构造函数内部或者原型方法内部建立。 建议只在构造函数中建立全部的实例属性,保证变量声明在一个地方完成。

function Person () {
 this.family = ['father','mother']
}
var p1 = new Person()
var p2 = new Person()
p1.family.push('girlFriend')

p1.family // ["father", "mother", "girlFriend"]
p2.family // ["father", "mother"]
复制代码

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

若是你还有印象,以前的构造函数模式不就是建立的实例属性和方法吗?因此,结合使用这两种方式,是目前ECMAScript中使用最普遍、认同度最高的建立自定义类型的方法。

function Person () {
    this.family = ['father','mother']
}
Person.prototype.age = 24
var p1 = new Person()
var p2 = new Person()
p1.family.push('girlFriend')
p1.family // ["father", "mother", "girlFriend"]
p2.family // ["father", "mother"]
p1.age = 25
p2.age // 25 
复制代码

以上示例,既有原型对象的共享属性,也有实例自身的属性,各得其所。

动态原型模式

可是,上面示例在混合使用两种模式时,依然是割裂开的,两种模式并无在一个方法中完成。而动态原型模式,正是来解决这个问题。

function Person () {
    this.age = 24
    if(typeof this.getAge !== 'function'){
        Person.prototype.getAge = function () {
            return this.age
        }
    }
}
复制代码

能够看到,咱们把原型对象模式的定义语句移动到了构建函数中,显式的将两种模式统一在了一块儿。

寄生构造函数模式

以前咱们说过,尽可能不要改动原生对象,可是若是想在原生对象上增长方法怎么办?咱们能够在原生对象的基础上,增长方法,而后生成一个新的对象。这就是寄生构造函数模式。

function ArrayPlus () {
    var plus = []
    plus.pipeStr = function () {
        return this.join('|')
    }
    return plus
}

var plus1 = new ArrayPlus()
plus1.push('red')
plus1.push('black')
plus1.pipeStr() // red|black

plus1.constructor // Array
Object.getPrototypeOf(Object.getPrototypeOf(plus1)) === Array.prototype // true
plus1 instanceof ArrayPlus // false
plus1 instanceof Aarray // true
复制代码

可是,生成的实例跟构造函数和原型对象是彻底没有联系的,而且也没法经过instanceof肯定其类型。因此,在其余模式可用的状况下,不推荐使用这个模式。

稳妥构造函数模式

稳妥对象是指没有公共属性,而且其方法也不引用this的对象。适合一些安全的环境。下面的示例中,除了对象提供的方法,是没有其余途径得到对象内部的原始数据的。 当前与寄生构造函数模式同样,生成的实例跟构造函数和原型对象是彻底没有联系的,而且也没法经过instanceof肯定其类型。

function Person (age) {
    return {
        getAge: function () {
            return age
        }
    }
}
var p1 = Person(12)
p1.getAge() // 12

Object.getPrototypeOf(p1) === Object.prototype // true
p1 instanceof Person // false
复制代码

继承

经过原型对象和构造函数相结合的模式,咱们能够批量的生成对象,这种模式能够称之为ECMAScript中的「类」;

那若是要批量生成「类」呢?这就要用到「继承」了。ECMAScript中的继承主要是依赖原型链来实现。

原型链

原型链的基本思想是利用原型让一个引用类型继承另外一个引用类型的属性和方法。实际作法就是:

  1. 让一个构造函数A的原型对象A.prototype指向另外一个构造函数B的实例b1,此时A.prototype === b1,那么构造函数A的实例a1会拥有构造函数B的原型对象B.prototype的全部属性和方法。
  2. 若是构造函数B的原型对象B.prototype刚好又指向另外一个构造函数C的实例c1,即B.prototype === c1。那么构造函数B的实例b1会拥有构造函数C的原型对象C.prototype的全部属性和方法。
  3. 如此层层递进,构造函数A的实例a1会同时拥有构造函数B的原型对象B.prototype和构造函数C的原型对象C.prototype的全部属性和方法。

这就是原型链的基本概念。代码实例以下:

function Grandpa () {}
Grandpa.prototype.sayHello = function () {
    return 'hello'
}
function Father () {}
Father.prototype = new Grandpa()
Father.prototype.sayWorld = function () {
    return 'world'
}
function Son () {}
Son.prototype = new Father()
var son1 = new Son()
son1.sayHello() // hello
son1.sayWorld() // world
复制代码

若是你还记得以前的原型搜索机制(仍是下面这张图),那么原型链其实就是对这种机制的向下拓展。

寻找属性值

// 调用son1实例上的sayWorld方法
son1.sayWorld()
// 先在实例上寻找,没有
Object.getOwnPropertyNames(son1) // []
// 继续在实例的原型上寻找,也没有
Object.getOwnPropertyNames(Son.prototype) // []
// 继续在实例的原型的原型上寻找,找到了
Object.getOwnPropertyNames(Father.prototype) // ["sayWorld"]

// 一样的,调用son1实例上的sayWorld方法
son1.sayHello() 
// 先在实例上寻找,没有
Object.getOwnPropertyNames(son1) // []
// 继续在实例的原型上寻找,也没有
Object.getOwnPropertyNames(Son.prototype) // []
// 继续在实例的原型的原型上寻找,没有
Object.getOwnPropertyNames(Father.prototype) // ["sayWorld"]
// 继续在实例的原型的原型的原型上寻找,找到了
Object.keys(Grandpa.prototype) // ["constructor", "sayHello"]
复制代码

别忘记默认的类型

那原型链的尽头————实例的原型的原型的原型...的原型是谁呢? 全部函数的默认原型都是Object的实例,因此全部自定义类型都继承了Object.prototype上的属性和方法。

Object.getPrototypeOf(Son.prototype) === Father.prototype // true
Object.getPrototypeOf(Father.prototype) === Grandpa.prototype // true
Object.getPrototypeOf(Grandpa.prototype) === Object.prototype // true
复制代码

Object的原型指向谁呢?

Object.getPrototypeOf(Object.prototype) // null
复制代码

Object.getPrototypeOf的返回值是传入对象继承的原型对象,因此,若是传入对象没有继承值,那么就返回null

肯定原型与实例的关系

instanceof 操做符。测试实例与原型链中的构造函数。

son1 instanceof Son // true
son1 instanceof Father // true
son1 instanceof Grandpa // true
son1 instanceof Object // true
复制代码

isPrototypeOf()方法。只要是原型链中出现过的原型,均可以说是该原型链所派生的实例的原型。

Son.prototype.isPrototypeOf(son1) // true
Father.prototype.isPrototypeOf(son1) // true
Grandpa.prototype.isPrototypeOf(son1) // true
Object.prototype.isPrototypeOf(son1) // true
复制代码

谨慎地定义方法

这一块在讲原型的时候也有说起,主要有两点:在原型链末端定义的重名属性或方法,会屏蔽掉在原型链顶端的定义;使用原型覆盖默认原型对象,要在添加原型的方法以前进行。

function Grandpa() {}
Grandpa.prototype.say = function () {
    return 'grandpa'
}
function Father() {}
Father.prototype = new Grandpa()
Father.prototype.say = function () {
    return 'father'
}
function Son () {}
Son.prototype.age = 12
Son.prototype = new Father()

var son1 = new Son()
son1.say() // father
son1.age // undefined
复制代码

另外,使用对象字面量的方式为原型添加方法,也会覆盖以前的原型对象。

原型链的问题

第一个问题以前在讲原型的时候也说过,就是若是在原型对象上定义一个引用类型的属性,可能出现问题。

第二个问题是在建立子类型的实例(son1)时,不能向超类型的构造函数(Grandpa)传递参数。

有鉴于此,通常不单独使用原型链。

借用构造函数

使用构造函数,能够解决上面提到的问题一。

function Grandpa () {
    this.family = ['house', 'car']
}
function Father () {
    // 使用call,完成实例属性继承
    // 其实就是以当前函数的做用域,替换目标函数做用域,并执行目标函数
    Grandpa.call(this)
    this.age = 26
}
// 工厂模式写法
// function Father () {
// var that = new Grandpa()
// that.age = 26
// return that
// }
var f1 = new Father()
var f2 = new Father()
f1.family.push('money')
f2.family // ['house', 'car']
复制代码

能够传递参数,解决了问题2

function Grandpa (name) {
    this.name = name
}
function Father (name) {
    Grandpa.call(this, name)
    this.age = 26
}
// 工厂模式写法
// function Father (name) {
// var that = new Grandpa(name)
// that.age = 26
// return that
// }
var f1 = new Father('jiahesheng')
f1.name // jiahesheng
f1.age // 26
复制代码

可是,借用构造函数也有本身的问题。也就是不能复用共享属性和方法了。

组合继承

其实就是结合了原型链和借用构造函数两种技术。

function Father (name) {
    this.name = name
}
Father.prototype.sayWorld = function () {
    return 'world'
}
function Son (name) {
    Father.call(this, name)
}
Son.prototype = new Father()
var s1 = new Son('zhu')
var s2 = new Son('sang')
s1.name // 'zhu'
s2.name // sang
s1.sayWorld() // 'world'
s2.sayWorld() // 'world'
复制代码

所谓的共享和自有,能够这么理解: 使用了原型链共享了属性和方法的实例,其实,就是包含了一堆指针,这些指针指向原型对象; 使用了借用构造函数技术拥有了自有的属性和方法的实例,其实,就是拥有了构造函数属性和方法的副本。

原型式继承

DC 最先提出,ECMAScript添加了Object.create方法规范化了这种模式。原型式继承的主要应用场景,就是返回一个对象,对象的原型指向传入的对象。

var person  = {
    age: 24
}
// from DC
function object (o) {
    function F() {}
    F.prototype = o
    return new F()
}
var p1 = object(person)
Object.getPrototypeOf(p1) === person // true
// from ECMAScript
var p2 = Object.create(person)
Object.getPrototypeOf(p2) === person // true
复制代码

Object.create还支持第二个参数,格式与Object.defineProperties相同

var person = {
    age: 24
}
var p1 = Object.create(person, {
    age: {
        value: 12
    }
})
p1.age  // 12
复制代码

Object.create最经常使用的方法仍是建立一个纯净的数据字典(没有原型对象的对象实例,即实例的原型指向null): Object.create(null)

var p3 = Object.create(null)
p3.__proto__ // undefined
Object.getPrototypeOf(p3) // null
复制代码

纯净的数据字典

使用Object.setPrototypeOf也能够实现:

var p4 = {}
Object.setPrototypeOf(p4, null)
p4.__proto__ // undefined
Object.getPrototypeOf(p4) // null
复制代码

寄生式继承

寄生式继承就是建立一个仅用于封装继承过程的函数,该函数在内部加强对象以后,会返回新的对象。寄生式继承的实际用途在下一节能更好的表示。

寄生组合式继承

咱们先来回顾一下,组合式继承:

function Father (name) {
    this.name = name || 'default'
}
Father.prototype.sayWorld = function () {
    return 'world'
}
function Son (name) {
    Father.call(this, name)
}
Son.prototype = new Father()
var s1 = new Son('zhu')
var s2 = new Son('sang')
s1.name // 'zhu'
s2.name // 'sang'
s1.sayWorld() // 'world'
s2.sayWorld() // 'world'

Son.prototype.name // 'default'
复制代码

它实现了属性和方法的自有和共享。可是,也带来了一些问题。

  1. 构造函数Father被调用执行了两次。一次在new Father(),一次在Father.call(this, name)
  2. 由于调用了两次,因此产生了多余的属性。Son.prototype = new Father()这个语句后,其实Son.prototype也拥有了name属性。只是咱们在使用name属性的时候,被实例上的name属性屏蔽了。

怎么解决这个问题呢?咱们将原型链继承这一步(Son.prototype = new Father())重写便可!避免调用new Father(),避免继承Father的实例属性和方法。 咱们能够组合使用寄生式继承和原型式继承,定义这样一个函数:

function inheritPrototype (prototypeObj, inheritor) {
    var prototype = Object.create(prototypeObj)
    prototype.constructor = inheritor
    inheritor.prototype = prototype
}
复制代码

inheritPrototype方法作了两件事:恢复了原型对象对构造函数的指针属性,「浅复制」了原型对象。以前咱们也说过,其实原型链的共享只是一堆指针的公用,指向的其实仍是一个原型对象。因此,「浅复制」恰好用上。

如今咱们把这个方法用起来!

function Father (name) {
    this.name = name
}
Father.prototype.sayWorld = function () {
    return 'world'
}
function Son (name) {
    Father.call(this, name)
}
inheritPrototype(Father.prototype, Son)
var s1 = new Son('zhu')
复制代码

到此咱们实现了最完美的继承!

ECMAScript 2015

历尽艰险,咱们终于使用ES5实现了「类」和「继承」,可是这相较于其余的面向对象的语言,看起来很不「规范」,而且实现起来也太麻烦。因此,在ECMAScript 2015版本中,使用classextend 关键字,更加「规范」的实现了「类」和「继承」。

咱们下篇文章继续探讨新的规范中的「面向对象」。

参考

《JavaScript高级程序设计第三版》

相关文章
相关标签/搜索