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]]
。也叫getter
、setter
。用于读、写对应属性的值。测试
[[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新加的方法,在此以前,对于getter
、setter
,浏览器内部有本身的实现。
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 Foo(...) 执行时,会发生如下事情:
Foo.prototype
的新对象被建立。Foo
,并将 this
绑定到新建立的对象。new Foo
等同于 new Foo()
,也就是没有指定参数列表,Foo
不带任何参数调用的状况。new
表达式的结果。若是构造函数没有显式返回一个对象,则使用步骤1建立的对象。(通常状况下,构造函数不返回值,可是用户能够选择主动返回对象,来覆盖正常的对象建立步骤)为了解决上面的问题,ECMAScript
语言中有了原型(prototype
)和原型链的概念。
每个函数上都有一个prototype
属性,这个属性是一个指针,指向一个对象,这个对象包含了一些属性和方法,这些属性和方法能够被全部由这个函数建立的实例所共享。
举例来讲,任意一个函数Person
的prototype
属性指向对象prototypeObject
对象,全部由new Person()
建立的实例(p1
、p2
...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
复制代码
因此,修改原型对象是把双刃剑,用得好能够解决问题,用很差就会带来问题。
原生对象(Object
、Array
、String
等)其实也是构造函数
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"]
复制代码
咱们在p1
的family
属性中添加了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
中的继承主要是依赖原型链来实现。
原型链的基本思想是利用原型让一个引用类型继承另外一个引用类型的属性和方法。实际作法就是:
A
的原型对象A.prototype
指向另外一个构造函数B
的实例b1
,此时A.prototype === b1
,那么构造函数A
的实例a1
会拥有构造函数B
的原型对象B.prototype
的全部属性和方法。B
的原型对象B.prototype
刚好又指向另外一个构造函数C
的实例c1
,即B.prototype === c1
。那么构造函数B
的实例b1
会拥有构造函数C
的原型对象C.prototype
的全部属性和方法。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'
复制代码
它实现了属性和方法的自有和共享。可是,也带来了一些问题。
Father
被调用执行了两次。一次在new Father()
,一次在Father.call(this, name)
。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')
复制代码
到此咱们实现了最完美的继承!
历尽艰险,咱们终于使用ES5
实现了「类」和「继承」,可是这相较于其余的面向对象的语言,看起来很不「规范」,而且实现起来也太麻烦。因此,在ECMAScript 2015
版本中,使用class
和 extend
关键字,更加「规范」的实现了「类」和「继承」。
咱们下篇文章继续探讨新的规范中的「面向对象」。
《JavaScript高级程序设计第三版》