大纲:
-
一、理解对象
- 1.1 属性类型
- 1.2 属性方法
-
二、创建对象
- 2.1 简单方式创建
- 2.2 工厂模式
- 2.3 构造函数
- 2.4 原型
-
三、继承
- 3.1 原型链
- 3.2 借用构造函数
- 3.3 组合继承(原型链+借用构造函数)
- 3.4 原型式继承
- 3.5 寄生式继承
- 3.6 寄生组合继承
- 3.6 总结
-
四、ES6继承
- Class关键字
- extends继承
- super关键字
- 原生构造函数拓展
- Mixin模式的实现
一、理解对象
ECMAScript中没有类的概念,因此它的对象也与基于类的语言的对象有所不同。
ECMA-262把对象定义为“无序属性的集合,其属性可以包含基本值,对象或者函数”。对象的每个属性或方法都有一个名字,而每个名字映射到一个值。我们可以把ECMAScript的对象想象成散列表:无非就是一组键值对,其值可以是数据或函数。
每个对象都是基于一个引用类型创建的,这个引用类型可以是原生类型,也可以是开发人员定义的类型。
1.1属性类型
ECMAScript中有两种属性:数据属性和访问器属性。
1.1.1数据属性
数据属性包含一个数据值的位置,在这个位置可以读取和写入值。数据属性有4个描述其行为的特征。
- [[Configurable]]:表示能否通过delete删除属性从而重新定义属性。能否修改属性的特性,或者能否把属性修改为访问器属性。直接在对象上定义的属性,它们的这个特性默认值为true。
- [[Enumerable]]:表示能否通过for-in循环返回属性。直接在对象上定义的属性,它们的这个特性默认值为true。
- [[Writable]]:表示能否修改属性的值。直接在对象上定义的属性,它们的这个特性默认为true。
- [[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为undefined。
要修改属性默认的特性,必须通过ES5的Object.defineProperty()方法。这个方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。其中,描述符(descriptor)对象的属性必须是:configurable、enumerable、writable和value。设置其中的一或多个值,可以更改对应的特征值。
var person = {};
Object.defineProperty(person, "name", {
writable: false, //不能修改属性的值....
configurable: false, //不能通过delete删除属性.....
value: "Jason" //写入属性值
});
console.log(person.name); //Jason
person.name = "Cor";
console.log(person.name); //Jason
delete person.name;
console.log(person.name); //Jason
注意,一旦把属性设置为不可配置的,就不能再把它更改为可配置的了。此时再调用
Object.defineProperty()方法修改除writable之外的特性就会导致错误。
var person = {};
Object.defineProperty(person, "name", {
configurable: false,
value: "Jason"
});
//抛出错误
Object.defineProperty(person, "name", {
comfogirable: true, //这行代码修改了特性导致报错
value: "Cor"
});
在调用Object.defineProperty()方法时,如果不指定configurable、enumerable和writable特性的默认值都是false。
1.1.2访问器属性
访问器属性不包含数据值,它包含一对getter和setter函数(不过,这两个函数都不是必须的)。在读取访问器属性时回调去getter函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter函数并传入新值,这个函数负责决定如何处理数据。访问器属性有如下4个特性:
- [[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。对于直接在对象上定义的属性,这个特性默认值为true。
- [[Enumerable]]:表示能否通过for-in循环返回属性。对于直接在对象上定义的属性,这个特性的默认值为true。
- [[Get]]:在读取属性时调用的函数。默认值为undefined。
- [[Set]]:在写入属性时调用的函数。默认值为undefined。
访问器属性不能直接定义,必须使用Object.defineProperty()方法来定义。
注意,一旦定义了取值函数get(或存值函数set),就不能将writable属性设为true,或者同时定义value属性,否则会报错。
var book = {
_year: 2004,
edition: 1
};
Object.defineProperty(book, "year", {
get: function() {
return this._year;
},
set: function(newValue) { //接受新值的参数
if(newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
});
book.year = 2005; //写入访问器,会调用setter并传入新值
console.log(book.edition); //2
var obj = {};
Object.defineProperty(obj, 'p', {
value: 123,
get: function() { return 456; }
});
// TypeError: Invalid property.
// A property cannot both have accessors and be writable or have a value
Object.defineProperty(obj, 'p', {
writable: true,
get: function() { return 456; }
});
// TypeError: Invalid property descriptor.
// Cannot both specify accessors and a value or writable attribute
1.2属性方法
- Object.getOwnPropertyDescriptor()
该方法可以获取属性描述对象。它的第一个参数是一个对象,第二个参数是一个字符串,对应该对象的某个属性名。注意,该方法只能用于对象自身的属性,不能用于继承的属性。
var obj = { p: 'a' };
Object.getOwnPropertyDescriptor(obj, 'p')
// Object { value: "a",
// writable: true,
// enumerable: true,
// configurable: true
// }
- Object.getOwnPropertyNames()
该方法返回一个数组,成员是参数对象自身的全部属性的属性名,不管该属性是否可遍历。下面例子中,obj.p1是可遍历的,obj.p2是不可遍历的。但是Object.getOwnPropertyNames会将它们都返回。
var obj = Object.defineProperties({}, {
p1: { value: 1, enumerable: true },
p2: { value: 2, enumerable: false }
});
Object.getOwnPropertyNames(obj)
// ["p1", "p2"]
与Object.keys的行为不同,Object.keys只返回对象自身的可遍历属性的全部属性名。下面代码中,数组自身的length属性是不可遍历的,Object.keys不会返回该属性。第二个例子的Object.prototype也是一个对象,所以实例对对象都会继承它,它自身的属性都是不可遍历的。
Object.keys([]) // []
Object.getOwnPropertyNames([]) // [ 'length' ]
Object.keys(Object.prototype) // []
Object.getOwnPropertyNames(Object.prototype)
// ['hasOwnProperty',
// 'valueOf',
// 'constructor',
// 'toLocaleString',
// 'isPrototypeOf',
// 'propertyIsEnumerable',
// 'toString']
- Object.defineProperty(),Object.defineProperties()
Object.defineProperty()方法允许通过属性描述对象,定义或修改一个属性,然后返回修改后的对象。实例上面已经介绍。
如果一次性定义或修改多个属性,可以使用Object.defineProperties方法。
var obj = Object.defineProperties({}, {
p1: { value: 123, enumerable: true },
p2: { value: 'abc', enumerable: true },
p3: { get: function () { return this.p1 + this.p2 },
enumerable:true,
configurable:true
}
});
obj.p1 // 123
obj.p2 // "abc"
obj.p3 // "123abc"
- Object.prototype.propertyIsEnumerable()
实例对象的propertyIsEnumerable方法返回一个布尔值,用来判断某个属性是否可遍历。
var obj = {};
obj.p = 123;
obj.propertyIsEnumerable('p') // true
obj.propertyIsEnumerable('toString') // false
二、创建对象
2.1 简单方式创建
我们可以通过new的方式创建一个对象,也可以通过字面量的形式创建一个简单的对象。
var obj = new Object();
或
var obj = {};
//为对象添加方法,属性
var person = {};
person.name = "TOM";
person.getName = function() {
return this.name;
}
// 也可以这样
var person = {
name: "TOM",
getName: function() {
return this.name;
}
}
这种方式创建对象简单,但也存在一些问题:创建出来的对象无法实现对象的重复利用,并且没有一种固定的约束,操作起来可能会出现这样或者那样意想不到的问题。如下面这种情况。
var a = new Object;
var b = new Object;
var c = new Object;
c[a]=a;
c[b]=b;
console.log(c[a]===a); //输出什么 false
该题的详细解析请参考文章一条面试题
2.2 工厂模式
当我们需要创建一系列相似对象时,显然上面简单的对象创建方式已经不可以了,这会使代码中出现很对重复的编码,造成代码冗余难维护。就以person对象为例,假如我们在实际开发中,需要一个名字叫做TOM的person对象,同时还需要另外一个名为Jake的person对象,虽然它们有很多相似之处,但是我们不得不重复写两次。没增加一个新的person对象,就重复一遍代码,听起来就是很崩溃的。
var perTom = {
name: 'TOM',
age: 20,
getName: function() {
return this.name
}
};
var perJake = {
name: 'Jake',
age: 22,
getName: function() {
return this.name
}
}
我们可以使用工厂模式的方式解决这个问题。顾名思义,工厂模式就是我们提供一个模子,然后通过这个模子复制出我们需要的对象。需要多少,就复制多少。
var createPerson = function(name, age) {
// 声明一个中间对象,该对象就是工厂模式的模子
var o = new Object();
// 依次添加我们需要的属性与方法
o.name = name;
o.age = age;
o.getName = function() {
return this.name;
}
return o;
}
// 创建两个实例
var perTom = createPerson('TOM', 20);
var PerJake = createPerson('Jake', 22);
工厂模式帮助我们解决了重复代码的问题,可以快速的创建对象。但是这种方式仍然存在两个问题:没有办法识别对象实例的类型
var obj = {};
var foo = function() {}
console.log(obj instanceof Object); // true
console.log(foo instanceof Function); // true
console.log(perTom instancceof (类名??)); //发现好像并不存在一个Person类
因此,在工厂模式的基础上,我们需要使用构造函数的方式来解决这个问题。
2.3 构造函数
2.3.1 new关键字
在Javascript中,new关键字十分神奇,可以让一个函数变的与众不同。看下面这个例子。
function demo() {
console.log(this);
}
demo(); // window,严格模式下this指向undefined
new demo(); // demo
从这个例子我们可以看到,使用new之后,函数内部发生了一些变化,this指向发生了改变。那么new关键字到底都做了什么事情呢?
// 先一本正经的创建一个构造函数,其实该函数与普通函数并无区别
var Person = function(name, age) {
this.name = name;
this.age = age;
this.getName = function() {
return this.name;
}
}
// 将构造函数以参数形式传入
function New(func) {
// 声明一个中间对象,该对象为最终返回的实例
var res = {};
if (func.prototype !== null) {
// 将实例的原型指向构造函数的原型
res.__proto__ = func.prototype;
}
// ret为构造函数执行的结果,这里通过apply,将构造函数内部的this指向修改为指向实例对象res
var ret = func.apply(res, Array.prototype.slice.call(arguments, 1));
// 当我们在构造函数中明确指定了返回对象时,那么new的执行结果就是该返回对象(即在构造函数中明确写了return this;)
if ((typeof ret === "object" || typeof ret === "function") && ret !== null) {
return ret;
}
// 如果没有明确指定返回对象,则默认返回res,这个res就是实例对象
return res;
}
// 通过new声明创建实例,这里的p1,实际接收的正是new中返回的res
var person1 = New(Person, 'tom', 20);
console.log(person1.getName());
// 当然,这里也可以判断出实例的类型了
console.log(p1 instanceof Person); // true
JavaScript内部会通过一些特殊处理,将var p1 = New(Person, ’tom’, 20);等效于var person1 = new Person(’tom’, 20); 我们熟悉的这种形式。具体是怎么处理的,暂时没法作出解释,需要更深入的了解原理。
当构造函数显示的return,会出现什么情况?
我们先来列出几种返回的情况,看一下返回什么结果:
//直接 return
function A(){
return;
}
//返回 数字类型
function B(){
return 123;
}
//返回 string类型
function C(){
return "abcdef";
}
//返回 数组
function D(){
return ["aaa", "bbb"];
}
//返回 对象
function E(){
return {a: 2};
}
//返回 包装类型
function F(){
return new Number(123);
}
//结果是:
A {}
B {}
C {}
["aaa", "bbb"]
Object {a: 2}
Number {[[PrimitiveValue]]: 123}
A {}
结合构造函数我们来看一下结果:
function Super (a) {
this.a = a;
return 123;
}
Super.prototype.sayHello = function() {
alert('hello world');
}
function Super_ (a) {
this.a = a;
return {a: 2};
}
Super_.prototype.sayHello = function() {
alert('hello world');
}
new Super(1);
new Super_(1);
//结果
Super {a: 1} 具有原型方法sayHello
Object {a: 2}
总结一下:在构造函数中 return 基本类型不会影响构造函数的值,而 return 对象类型 则会替代构造函数返回该对象。
2.3.2 构造函数创建对象
为了能够判断实例与对象的关系,我们就使用构造函数来搞定。
像Object和Array这样的原生构造函数,在运行时自动出现在执行环境中。我们也可以创建自定义的构造函数,从而定义对象类型的属性和方法。例如,
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.getName = function() {
console.log(this.name);
}
}
var person1 = new Person("Jason", 18, "WEB”);
var person2 = new Person("Cor", 19, "WEB");
console.log(person1.getName()); //Jason
console.log(person1 instanceof Person); //true
构造函数模式和工厂模式存在一下不同之处:
- 没有显示的创建对象(new Object() 或者 var a = {})
- 直接将属性和方法赋给this对象
- 没有return语句
关于构造函数,如果你暂时不能够理解new的具体实现,就先记住下面这几个结论:
- 与普通函数相比,构造函数并没有任何特别的地方,首字母大写只是我们开发中的约定规定,用于区分普通函数
-
new关键字让构造函数拥有了与普通函数不同的许多特点,new的过程中,执行了下面的过程:
- 声明一个中间对象,即实例对象
- 将该中间对象的原型指向构造函数原型(res.__proto__ = func.prototype)
- 将构造函数this,指向该中间对象
- 返回该中间对象,及返回实例对象
2.3.3 把构造函数当普通函数
//当作构造函数使用
var person = new Person("Jason", 18, "web");
person.getName(); //“Jason"
//作为普通函数调用
Person("Cor", 19, "web"); //添加到window
window.getName(); //“cor"
//在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "Kristen", 22, "web");
o.getName(); //"kriten"
当在全局作用域中调用一个函数时,this对象总是指向Global对象(在浏览器中的window对象),最后使用了call() ( 或者apply() )在某个特殊对象的作用域中调用Person()函数。这里是在对象o的作用域调用的,因此调用后o就拥有了所有属性和方法。
2.3.4 构造函数的问题
构造函数的主要问题:上述例子中,每一个getName方法实现的功能其实是一模一样的,但是由于分别属于不同的实例,就不得不一直不停的为getName分配空间。
person1.getName == person2.getName; //false
我们对构造函数稍加修改,在构造函数内部我们把getName属性设置成等于全局的getName函数。由于构造函数的getName属性包含的是一个指向函数的指针,因此person1和person2对象就共享了在全局作用域中定义的同一个getName()函数。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.getName = getName;
}
function getName() {
console.log(this.name);
}
var person1 = new Person("Jason", 18, "WEB");
var person2 = new Person("Cor", 19, "WEB”);
person1.getName == person2.getName; //true
2.4 原型
我们创建的每一个函数,都可以有一个prototype属性,该属性指向一个对象,这个对象就是我们说的原型对象。原型对象的用途是:包含所有可以由构造函数实例共享的属性和方法。按照字面理解就是,prototype就是由构造函数创建的实例对象的原型对象,使用原型对象的好处就是可以让所有实例共享原型对象所包含的方法,属性。
2.4.1 理解原型对象
上面说了,每一个函数创建的时候,都会依据某些规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型的对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。以上面的例子来说,也就是Person.prototype.constructor指向Person。
创建了自定义构造函数之后,其原型对象默认只会取得constructor属性;至于其它方法,则都是从Object继承而来的。当调用构造函数new一个新实例后(person1) ,实例都有一个__proto__属性,该属性指向构造函数的原型对象(Person.prototype),通过这个属性,让实例对象也能够访问原型对象上的方法。因此,当多有的实例都能够通过__proto__访问到原型对象时,原型对象的方法与属性就变成了共有方法与属性。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.getName = function() {
console.log(this.name);
}
var person1 = new Person("Jason", 20);
var person2 = new Person("Tim", 40);
console.log(person1.getName == person2.getName); //true
ECMA-262第五版中管这个指针叫[[Prototype]],虽然在脚本中没有标准的方式访问[[Prototype]],单Firefox,Safari和Chrome在每个对象上都支持一个属性__proto__;而在其他实现中,这个属性对脚本则是完全不可见的。不过需要明确的真正重要一点就是,这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。
虽然所有的实现中都无法访问到[[Prototype]],但是可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系。从本质上讲,如果[[Prototype]]指向调用isPrototypeOf()方法的对象(Person.prototype),那么这个方法就会返回true。
console.log(Person.prototype.isPrototypeOf(person1)) //true
console.log(Person.prototype.isPrototypeOf(person2)) //true
ES5增加了一个新方法,叫Object.getPrototypeOf(),在所有支持的实现中,这个方法返回[[Prototype]]的值,可以方便的获取一个对象的原型。
console.log(Object.getPrototypeOf(person1) == Person.prototype); //true
console.log(Object.getPrototypeOf(person1).getName()); //"Jason"
搜索机制:
当我们访问对象的属性或者方法时,会优先访问实例对象自身的属性和方法。当代码执行到读取对象的属性a时,都会执行一次搜索。搜索首先从对象的实例本身开始。如果在实例中找到属性a,则返回该属性的值;如果没找到,则继续搜索之震惊指向的原型对象,在原型对象中查找属性a,如果在原型中找到这个属性,则返回该属性。简单的说,就是会一层层搜索,若搜索到则返回,没搜索到则继续下层搜索。
虽然可以通过实例访问原型的值,但是却不能通过对象实例重写原型的值。如果我们为水添加了一个属性,并且该属性名和实例原型中的一个属性同名,就会在实例中创建该属性,该属性户屏蔽原型中的相同属性。因为搜索的时候,首先在实例本身搜索,查找到后直接返回实例中属性的值,不会搜索到原型中。
function Person() {
}
Person.prototype.name = "Jason";
Person.prototype.age = 29;
Person.prototype.job = "Web";
Person.prototype.getName = function() {
console.log(this.name);
}
var person1 = new Person();
var person2 = new Person();
person1.name = "Cor";
person1.getName(); //"Cor"
person2.getName(); //"Jason"
若想能够访问原型中的属性,只要用delete操作符删掉实例中的属性即可。
delete person1.name;
person1.getName(); //"Jason"
hasOwnProperty()
该方法可以检测一个属性是存在在实例中,还是存在于原型中。这个方法继承于Object,只有在给定属性存在于对象实例中时,才会返回true
console.log(person1.hasOwnProperty("name")); //false
person1.name = "Cor";
console.log(person1.hasOwnProperty("name")); //true;
2.4.2 in操作符
in操作符有两种使用方式:单独使用和在for-in循环中使用。
在单独使用时,in操作符在通过对象能访问到给定属性时就会返回true,无论属性存在于实例还是原型中。
console.log("name" in person1); true
in的这种特殊性做常用的场景之一,就是判断当前页面是否在移动端打开。
isMobile = 'ontouchstart' in document;
// 很多人喜欢用浏览器UA的方式来判断,但并不是很好的方式
2.4.3 更简单的原型语法
可以用一个包含所有属性和方法的对象字面量来重写整个原型对象。
function Person(){}
Person.prototype = {
name : "Jason",
age : 29,
job : "Web",
getName : function() {
console.log(this.name)
}
};
用对象字面的方法和原来的方法会有区别:constructor属性不再指向Person了。因为这种写法,本质上是修改了Person.prototype对象的引用,将引用从原来的默认值修改为了这个新对象{}的引用,constructor属性也变成了新对象{}的constructor属性(指向Object构造函数),不再指向Person。尽管instanceof操作符能返回正确的结果,但是constructor已经无法确定对象的类型了。
var friend = new Person();
console.log(friend instanceof Object); //true
console.log(friend instanceof Person); //true
console.log(friend.constructor == Person); //false
console.log(friend.constructor == Object); //true
如果construct的值很重要,我们可以像下面这样特意将它设置回适当的值。
function Person(){}
Person.prototype = {
constructor: Person,
name : "Jason",
age : 29,
job : "Web",
getName : function() {
console.log(this.name)
}
};
2.4.4 原型的动态性
由于在原型中查找值的过程是一次搜索,因此我们在原型对象上所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也一样。下面这个例子中,friend实例是在添加sayHi方法之前创建的,但它仍然可以访问新方法。这是因为实例与原型之间只不过是一个指针,而非一个副本,因此就可以在原型中找到新的sayHi属性并返回值。
var friend = new Person();
Person.prototype.sayHi = function() {
alert("hi");
};
friend.sayHi(); //"hi"
但是,如果是通过{}这种重写原型对象的情况,就和上边不一样了。因为new实例时,实例中的__proto__属性指向的是最初原型,而把原型修改为新的对象{}就等于切断了构造函数与最初原型之间的联系,同时实例中仍然保存的是最初原型的指针,因此无法访问到构造函数的新原型中的属性。请记住:实例只与原型有关,与构造函数无关。
function Person(){}
var friend = new Person();
Person.prototype = {
constructor: Person,
name : "Jason",
age : 29,
job : "Web",
sayName : function() {
console.log(this.name)
}
};
friend.sayName(); //error,friend.sayName is not a function
如图,重写原型对象切断了现有原型与任务之前已经存在的对象实例之间的联系;friend实例引用的仍然是最初的原型,因此访问不到sayName属性。
注意,若想使用对象字面量重写原型,要在创建实例之前完成。
2.4.5 原生对象的原型
原型创建对象的重要性不仅体现在创建自定义对象方面,就连所有原生的引用类型都采用这种模式创建。所有原生引用类型(Object、Array、String,等等)都在其构造函数的原型上定义了方法。
console.log(Array.prototype.sort); //function(){…}
console.log(String.prototype.substring); //function(){...}
通过原生对象的原型,我们也可以自定义新的方法。
String.prototype.startsWith = function(text) {
return this.indexOf(text) == 0;
};
var msg = "Hello world!";
console.log(msg.startsWith("Hello")); //true
巩固一下原型相关的知识点,我们以window这个对象为例,来查看一下各个属性值
三、继承
3.1原型链
原型对象其实也是普通的对象。几乎所有的对象都可能是原型对象,也可能是实例对象,而且还可以同时是原型对象与实例对象。这样的一个对象,正是构成原型链的一个节点。
我们知道所有的函数都有一个叫做toString的方法,那么这个方法到底是从哪来的呢?先声明一个函数:function add() {};,通过下图来看一下这个函数的原型链情况。
其中add是Function对象的实例。而Function的原型对象同时又是Object原型的实例。这样就构成了一条原型链。原型链的访问,其实跟作用域链有很大的相似之处,他们都是一次单向的查找过程。因此实例对象能够通过原型链,访问到处于原型链上对象的所有属性与方法。这也是foo最终能够访问到处于Object原型对象上的toString方法的原因。
我们再来看一个例子:
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
}
function SubType(){
this.subproperty = false;
}
//继承了SuperType
//SubType的原型对象等于SubperType的实例,
//这样SubType内部就会有一个指向SuperType的指针从而实现继承
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function(){
return this.subproperty;
}
var instance = new SubType();
console.log(instance.getSuperValue()); //true
SubType继承了superType,而继承是通过创建SuperType实例,并将该实例赋给SubType.prototype实现的。原来存在于SuperType的实例中的所有属性和方法,现在也存在于SubType.prototype中了。这个例子中的实例、构造函数和原型之间的关系如图:
注意,instance.constructor现在指向的是SuperType,是因为subtype的原型指向了另一个对象SuperType的原型,而这个原型对象的constructor属性指向的是SuperType。
通过实现原型链,本质上扩展了前面说的原型搜索机制。在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续向上。那上面这个例子来说,调用instance.getSuperValue()会经历三个搜索步骤:
- 搜索instance实例;未搜到;
- 搜索SubType.prototype,未搜索到;
- 搜索SuperType.prototype,找到该属性,返回值。
3.1.1 默认原型
所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内布指针,指向Object.prototype。这也正是所有自定义类型都会继承toString()、valueOf()等默认方法的根本原因。所以上面的例子展示的原型链中还应该包含另一个继承层次。
3.1.2 确定原型和实例的关系
有两种方式可以确认:instanceof操作符以及isPrototypeOf()方法。
console.log(instance instanceof Object); //true
console.log(instance instanceof SuperType); //true
console.log(instance instanceof SubType); //true
console.log(Object.prototype.isPrototypeOf(instance)); //true
console.log(SuperType.prototype.isPrototypeOf(instance)); //true
console.log(SubType.prototype.isPrototypeOf(instance)); //true
由于原型链的关系,可以说instance是Object,SuperType,SubType中任何一个类型的实例,因此都返回true。同样的,只要是原型链中出现过的原型,都可以说是该原型链所派生实例的原型。
3.1.3 谨慎定义方法
子类型(SubType)有时候需要重写超类型(SuperType)中的某个方法,或者需要添加超类型中没有的方法,但不管怎样,给原型添加方法一定要在替换原型语句之后。避免出现2.4.4中出现的问题。
另外要注意,在通过原型链实现继承时,不能使用对象字面量创建原型方法,这样做会重写原型。
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
}
function SubType(){
this.subproperty = false;
}
//继承超类型
SubType.prototype = new SuperType();
//添加新方法
SubType.prototype.getSubValue = function(){
return this.subproperty;
}
//重写超类型中的方法
SubType.prototype.getSuperValue = function(){
return false;
}
//使用字面量添加新方法,会导致上面代码无效
SubType.prototype = {
getSubValue : function(){
return this.subproperty;
},
someOtherMethod : function(){
return false;
}
}
3.1.4 原型链的问题
function SuperType(){
this.colors = ["red", "blue", "green"];
}
function SubType(){
}
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); //"red, blue, green, black"
var instance2 = new SubType();
console.log(instance2.colors); //"red, blue, green, black"
这个例子中的SuperType构造函数定义了一个colors属性,该属性包含一个数组(引用类型值)。SuperType的每个实例都会有各自包含自己数组的colors属性。当SubType通过原型链继承了SuperType之后,SubType.prototype就变成了SuperType(),所以它也用用了一个colors属性。就跟专门创建了一个SubType.prototype.colors属性一样。结果SubType得所有实例都会共享这一个colors属性。
原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。所以在实际运用中很少会单独使用原型链。
3.2 借用构造函数
在子类型构造函数中调用超类型构造函数。函数只不过是在特定环境中执行代码的对象,因此通过使用apply()和call()方法也可以在新创建的对象上执行构造函数。
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
function SubType(){
//继承了SuperType,同时还传递了参数
SuperType.call(this, "Jason”);
//执行上边这个语句,相当于把SuperType构造函数的属性在这里复制了一份
//this.name = "Jason”;
//this.colors = ["red", "blue", "green"];
//实例属性
this.age = 18;
}
var instance1 = new SubType();
instance1.colors. push("black");
console.log(instance1.colors); //"red,blue,green,black"
console.log(instance1.name); //"Jason"
console.log(instance1.age); //18
var instance2 = new SubType();
console.log(instance2.colors); //"red,blue,green"
为了确保SuperType构造函数不会重写子类型属性,可以在调用超类型构造函数之后,再添加需要在子类型的定义的私有属性。
这个方式实现继承仍然存在问题:
- 方法都在构造函数中定义,每个实例创建后都会为构造函数的属性分配自己的内存,复用方法就无从谈起。
- 而且,即使在超类型的原型中定义了公有属性,但这些属性对于子类型而言是不可见的,所以采用这种方式继承,就要把所有属性写在构造函数中
所以这种方式在实际开发中是很少单独这样使用的。
3.3 组合继承(原型链+借用构造函数)
组合继承(combination inheritance),有时候也叫做经典继承,指的是将原型链和借用构造函数的继承方式组合到一块,从而发挥二者的长处的一种继承方式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样既可以通过在原型上定义方法实现了函数复用,又可以在构造函数中定义方法保证每个实例都有自己的私有属性。
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
};
function SubType(name, age){
//继承属性
SuperType.call(this, name);
this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
console.log(this.age);
};
var instance1 = new SubType("Jason", 18);
instance1.colors.push("black");
console.log(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Jason"
instance1.sayAge(); //18
var instance2 = new SubType("Cor", 20);
console.log(instance2.colors) //"red,blue,green"
instance2.sayName(); //"Cor"
instance2.sayAge(); //20
组合继承的问题
组合继承虽然是现在javascript最常用的继承模式,但是它也有不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型的构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数的内部。
function SuperType(name){
this.name = name;
this.colors = ["red", "bule", "grenn"];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
}
function SubType(name, age){
SuperType.call(this, name); //第二次调用SuperType
this.age = age;
}
SubType.prototype = new SuperType(); //第一次调用SuperType
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
console.log(this.age);
}
有注释的两行代码是调用SuperType构造函数的代码,第一次调用SuperType构造函数时,SubType.prototype会有SuperType的实例属性。第二次调用SuperType的构造函数时SubType会在构造函数中添加了SuperType的实例属性。当创建SubType的实例它的[[Prototype]]和自身上都有相同属性。根据搜索机制自身的属性就会屏蔽SubType原型对象上的属性。等于原型对象上的属性是多余的了。如图所示,有两组name和colors属性:一组在实例上,一组在Subtype原型中。这就是调用两次构造函数的结果。
3.4 原型式继承
基本思想是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
function object(o){
function F(){}
F.prototype = o;
return new F();
}
在object()函数内部,先创建了一个临时性的构造函数,然后传入的对象作为这个构造函数的原型,最后返回了这个临时构造函数的一个新实例。从本质上讲,object()对传入其中的对象执行了一次复制。
在ECMAScript5通过新增Object.create()方法规范了原型式继承。这个方法接收两个参数:第一个用于做新对象原型的对象;第二个参数可选,为新对象定义额外的属性的对象。在传入一个参数的情况下,
Object.create()和object()方法的行为相同。
var person = {
name: "Jason",
friends: ["Cor", "Court", "Sam"]
};
var anotherPerson = Object.create(person);
anotherPerson.friends.push("Rob");
var yetAnotherPerson = Object.create(person, {
name: {
value: "Greg"
}
});
yetAnotherPerson.friends.push("Barbie");
console.log(yetAnotherPerson.name); //"Greg"
console.log(anotherPerson.name); //"Jason"
console.log(person.friends); //"Cor,Court,Sam,Rob,Barbie"
console.log(anotherPerson.__proto__); //"Cor,Court,Sam,Rob,Barbie"
这种原型式继承,要求必须有一个对象可以作为另一个对象的基础,把这个它传递给object()对象,然后再根据需求对得到的对象加以修改。在这个例子中,person对象可以作为另一个对象的基础,把它传入object()函数后返回一个新对象。这个新对象是将person作为原型,这意味着person.friend不仅属于person,而且被anotherPerson和yetAnotherPerson共享。
3.5 寄生式继承
寄生式(parasitic)继承是与原型式继承紧密相关的一种思路。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。
function createAnother(original){
var clone = object(original); //通过调用函数创建一个新对象
clone.sayHi = function(){ //以某种方式来增强这个对象
alert("Hi");
};
return clone; //返回这个对象
}
//使用createAnother
var person = {
name: "Jason",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"
这个例子中的代码基于person对象返回了一个新对象——anotherPerson。新对象不仅具有person的所有属性和方法,而且还有自己的sayHi()方法。
3.6 寄生组合式继承
所谓寄生组合式继承,即通过借用构造函数来继承实例属性,通过寄生式继承方式来继承原型属性。其基本思路就是:不必为指定子类型的原型二调用超类型的构造函数,我们需要的只是超类型原型的一个副本而已。本质上就是,使用寄生式继承超类型的原型,然后将结果指定给子类型的原型。寄生组合式继承的基本模式如下:
function inheritPrototype(subType, superType){
var prototype = object(superType.prototype); //创建对象
prototype.constructor = subType; //增强对象
subType.prototype = prototype //指定对象
}
这个实例的inheritPrototype()函数实现了寄生组合式继承的最简单形式。这个函数接受两个参数:子类型构造函数和超类型构造函数。在函数内部,第一步是创建超类型原型的一个副本。第二步是为创建的副本添加constructor属性,从而弥补因重写原型而失去的默认的constructor属性。最后一步,将新创建的对象(即副本)赋值给子类型的原型。
function object(o){
function F(){} //创建个临时构造函数
F.prototype = o; //superType.prototype
return new F(); //返回实例
}
function inheritPrototype(subType, superType){
/* 创建对象
传入超类型的原型,通过临时函数进行浅复制,F.prototype的指针就指向superType.prototype,在返回new F()
*/
var prototype = object(superType.prototype);
prototype.constructor = subType; //增强对象
/* 指定对象
子类型的原型等于F类型的实例,当调用构造函数创建一个新实例后,该实例会包含一个[[prototype]]的指针指向构造函数的原型对象,所以subType.prototype指向了超类型的原型对象这样实现了继承,因为构造函数F没有属性和方法这样就子类型的原型中就不会存在超类型构造函数的属性和方法了。
*/
subType.prototype = prototype //new F();
}
function SuperType(name){
this.name = name;
this.colors = ["red", "bule", "grenn"];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
}
function SubType(name, age){
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
//即等价于:
SubType.prototype = Object.create(Super.Prototype);
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
console.log(this.age);
}
var ins1 = new SubType("Jason", 18);
3.7 总结
这里我们对六种继承方式的基本思想,具体实现,优缺点做一个简单的总结,巩固一下我们上面学到的知识。
继承方式:原型链继承
基本思想:利用原型链来实现继承,超类的一个实例作为子类的原型
具体实现:
// 子类
function Sub(){
this.property = ‘Sub Property’;
}
Sub.prototype = new Super();
// 注意这里new Super()生成的超类对象并没有constructor属性,故需添加上
Sub.prototype.constructor = Sub;
优缺点:
优点:
- 简单明了,容易实现
- 实例是子类的实例,实际上也是父类的一个实例
- 父类新增原型方法/原型属性,子类都能访问到
缺点:
- 所有子类的实例的原型都共享同一个超类实例的属性和方法
- 在创建子类型的实例时,不能向超类型的构造函数传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数
继承方式:借用构造函数
基本思想:通过使用call、apply方法可以在新创建的对象上执行构造函数,用父类的构造函数来增加子类的实例
具体实现:
// 子类
function Sub(){
Super.call(this);
this.property = 'Sub Property’;
}
优缺点:
优点:
- 简单明了,直接继承超类构造函数的属性和方法
- 可以传递参数
缺点:
- 无法继承原型链上的属性和方法
- 实例只是子类的实例,不是父类的实例
继承方式:组合继承
基本思想:利用构造继承和原型链组合。使用原型链实现对原型属性和方法的继承,用借用构造函数模式实现对实例属性的继承。这样既通过在原型上定义方法实现了函数复用,又能保证每个实例都有自己的属性
具体实现:
// 子类
function Sub(){
Super.call(this);
this.property = 'Sub Property’;
}
Sub.prototype = new Super();
// 注意这里new Super()生成的超类对象并没有constructor属性,故需添加上
Sub.prototype.constructor = Sub;
优缺点:
优点:
- 解决了构造函数的两个问题
- 既是父类实例,也是子类实例
缺点:
- 调用两次超类型的构造函数,导致子类上拥有两份超类属性:一份在子类实例中,一份在子类原型上,且搜索时实例中属性屏蔽了原型中的同名属性
继承方式:原型式继承
基本思想:采用原型式继承并不需要定义一个类,传入参数obj,生成一个继承obj对象的对象
具体实现:
function object(obj){
function F(){};
F.prototype = obj;
return new F();
}
优缺点:
优点:
- 直接通过对象生成一个继承该对象的对象
缺点:
- 不是类式继承,而是原型式继承,缺少了类的概念
继承方式:寄生式继承
基本思想:创建一个仅仅用于封装继承过程的函数,然后在内部以某种方式增强对象,最后返回对象
具体实现:
function object(obj){
function F(){}
F.prototype = obj;
return new F();
}
function createSubObj(superInstance){
var clone = object(superInstance);
clone.property = 'Sub Property’;
return clone;
}
优缺点:
优点:
- 原型式继承的一种拓展
缺点:
- 依旧没有类的概念
继承方式:寄生组合式继承
基本思想:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法,不必为了指定子类型的原型而调用超类型的构造函数,只需要超类型的一个副本。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型
具体实现:
function inheritPrototype(Super,Sub){
var superProtoClone = Object.Create(Super.prototype);
superProtoClone.constructor = Sub;
Sub.prototype = superProtoClone;
}
function Sub(){
Super.call(this);
Sub.property = 'Sub Property’;
}
inheritPrototype(Super,Sub);
优缺点:
优点:
- 完美实现继承,解决了组合式继承带两份属性的问题
缺点:
- 过于繁琐,故不如组合继承
四、ES6继承
4.1 Class关键字
ES6中通过class关键字定义类。
class Parent {
constructor(name,age){
this.name = name;
this.age = age;
}
speakSomething(){
console.log("I can speek chinese");
}
}
// 经babel转码之后,代码是:
"use strict";
var _createClass = function () {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
return function (Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps);
if (staticProps) defineProperties(Constructor, staticProps);
return Constructor;
};
}();
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var Parent = function () {
function Parent(name, age) {
_classCallCheck(this, Parent);
this.name = name;
this.age = age;
}
_createClass(Parent, [{
key: "speakSomething",
value: function speakSomething() {
console.log("I can speek chinese");
}
}]);
return Parent;
}();
可以看出类的底层还是通过构造函数去创建的。
注意一点,通过ES6创建的类,是不允许直接调用的。即在ES5中,可以直接运行构造函数Parent()。但是在ES6中就不行,在转码的构造函数中有 _classCallCheck(this, Parent);语句,防止通过构造函数直接运行。直接在ES6运行Parent(),报错Class constructor Parent cannot be invoked without ‘new’,转码后报错Cannot call a class as a function。
转码中_createClass方法,它调用Object.defineProperty方法去给新创建的Parent添加各种属性。defineProperties(Constructor.prototype, protoProps)是给原型添加属性。如果你有静态属性,会直接添加到构造函数上defineProperties(Constructor, staticProps)。
4.2 extends继承
Class可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。
class Parent {
static height = 12
constructor(name,age){
this.name = name;
this.age = age;
}
speakSomething(){
console.log("I can speek chinese");
}
}
Parent.prototype.color = 'yellow'
//定义子类,继承父类
class Child extends Parent {
static width = 18
constructor(name,age){
super(name,age);
}
coding(){
console.log("I can code JS");
}
}
var c = new Child("job",30);
c.coding()
转码之后的代码变成了这样
"use strict";
var _createClass = function () {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
return function (Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps);
if (staticProps) defineProperties(Constructor, staticProps);
return Constructor;
};
}();
function _possibleConstructorReturn(self, call) {
if (!self) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return call && (typeof call === "object" || typeof call === "function") ? call : self;
}
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var Parent = function () {
function Parent(name, age) {
_classCallCheck(this, Parent);
this.name = name;
this.age = age;
}
_createClass(Parent, [{
key: "speakSomething",
value: function speakSomething() {
console.log("I can speek chinese");
}
}]);
return Parent;
}();
Parent.height = 12; //注意,该方法并不在转码后的构造函数function Parent中,
Parent.prototype.color = 'yellow';
//定义子类,继承父类
var Child = function (_Parent) {
_inherits(Child, _Parent);
function Child(name, age) {
_classCallCheck(this, Child);
return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name, age));
}
_createClass(Child, [{
key: "coding",
value: function coding() {
console.log("I can code JS");
}
}]);
return Child;
}(Parent);
Child.width = 18;
var c = new Child("job", 30);
c.coding();
可以看到,构造类的方法没变,只是添加了_inherits核心方法来实现继承,我们来重点分析一个这个方法做了什么。
- 首先判断父类的实例
- 然后执行
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
//这段代码翻译一下就是
function F(){}
F.prototype = superClass.prototype
subClass.prototype = new F()
subClass.prototype.constructor = subClass
- 最后,subClass.__proto__ = superClass。
_inherits方法的核心思想,总结一下就是下面这两句话:
subClass.prototype.__proto__ = superClass.prototype
subClass.__proto__ = superClass
那为什么这样一倒腾,它就实现了继承了呢?
首先 subClass.prototype.__proto__ = superClass.prototype保证了c instanceof Parent是true,Child的实例可以访问到父类的属性,包括内部属性,以及原型属性。其次,subClass.__proto__ = superClass,保证了Child.height也能访问到,也就是静态方法。
class Parent {}
class Child extends Parent {}
// for static propertites and methods
alert(Child.__proto__ === Parent); // true
// and the next step is Function.prototype
alert(Parent.__proto__ === Function.prototype); // true
// that's in addition to the "normal" prototype chain for object methods
alert(Child.prototype.__proto__ === Parent);
在内置对象中没有静态继承
请注意,内置类没有静态 [[Prototype]] 引用。例如,Object 具有 Object.defineProperty,Object.keys等方法,但 Array,Date 不会继承它们。
Date 和 Object 之间毫无关联,他们独立存在,不过 Date.prototype 继承于 Object.prototype,仅此而已。
造成这个情况是因为 JavaScript 在设计初期没有考虑使用 class 语法和继承静态方法。
4.3 super关键字
super这个关键字,既可以当函数使用,也可以当对象使用。在这两种情况下,它的用法完全不同。
- 第一种情况,super作为函数调用时代表父类的构造函数。ES6要求,子类的构造函数必须执行一次super函数。
子类B的构造函数之中的super()代表调用父类的构造函数,必须执行,否则会报错。
class A {}
class B extends A {
constructor() {
super();
}
}
注意,super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B的实例,因此super()在这里相当于A.prototype.constructor.call(this)。
new.target指向当前正在执行的函数。可以看到,在super()执行时,它指向的是子类B的构造函数,而不是父类A的构造函数。也就是说,super()内部的this指向的是B。
class A {
constructor() {
console.log(new.target.name);
}
}
class B extends A {
constructor() {
super();
}
}
new A() // A
new B() // B
注意,作为函数时,super()只能用在子类的构造函数之中,用在其他地方会报错。
class A {}
class B extends A {
m() {
super(); // 报错
}
}
- 第二种情况,super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
class A {
constructor() {
this.x = 2;
this.y = 8;
}
p() {
console.log(this.x);
},
}
A.prototype.z = 10;
class B extends A {
constructor() {
super();
this.x = 5;
console.log(super.p()); //5;
}
getY() {
return super.y;
}
getZ() {
return super.z;
}
}
let b = new B();
b.getY(); //undefined
b.getZ(); //10
在普通方法中,super指向A.prototype,所以super.p()就相当于A.prototype.p()。但是这里需要注意两点:
- super指向的是父类原型,所以定义在父类实例上的方法或属性,无法通过super调用。所以在B类的getY()方法中调用super.y获取不到值。但是定义在父类原型上的方法就可以获取到,如getZ()方法中。
- ES6规定,在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前子类的实例。在B类中调用super.p(),this指向B类实例,输出的结果为5。super.p()实际上执行的是super.p.call(this)。
由于this指向子类实例,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。
class A {
constructor() {
this.x = 1;
}
}
class B extends A {
constructor() {
super();
this.x = 2;
super.x = 3;
console.log(super.x); // undefined,super获取不到父类的实例属性
console.log(this.x); // 3
}
}
在静态方法中,super作为对象指向父类,而不是父类的原型。另外,在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。
class A {
constructor() {
this.x = 1;
}
static print() {
console.log(this.x);
}
}
class B extends A {
constructor() {
super();
this.x = 2;
}
static m() {
super.print();
}
}
B.x = 3;
B.m() // 3
注意,
- 使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。
class A {}
class B extends A {
constructor() {
super();
console.log(super); // 报错
}
}
- 由于对象总是继承其他对象的,所以可以在任意一个对象中,使用super关键字。
var obj = {
toString() {
return "MyObject: " + super.toString();
}
};
obj.toString(); // MyObject: [object Object]
在内置对象中没有静态继承
内置类没有静态 __proto__引用。例如,Object 具有 Object.defineProperty,Object.keys等方法,但 Array,Date 不会继承它们。
Date 和 Object 之间毫无关联,他们独立存在,不过 Date.prototype 继承于 Object.prototype,仅此而已。
造成这个情况是因为 JavaScript 在设计初期没有考虑使用 class 语法和继承静态方法。
4.4 原生构造函数拓展
原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript的原生构造函数大致有下面这些。
- Boolean()
- Number()
- String()
- Array()
- Date()
- Function()
- RegExp()
- Error()
- Object()
以前,原生构造函数是无法继承的。比如,不能自己定义一个Array的子类。
function MyArray() {
Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype, {
constructor: {
value: MyArray,
writable: true,
configurable: true,
enumerable: true
}
});
var colors = new MyArray();
colors[0] = "red";
colors.length // 0
colors.length = 0;
colors[0] // "red"
上面这个例子中定义了一个继承Array的MyArray类。但是,我们看到,这个类的行为与Array完全不一致。
之所以会发生这种情况,是因为子类无法获得原生构造函数的内部属性,通过Array.apply()或者分配给原型对象都不行。原生构造函数会忽略apply方法传入的this,也就是说,原生构造函数的this无法绑定,导致拿不到内部属性。
ES5 是先新建子类的实例对象this,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数。比如,Array构造函数有一个内部属性[[DefineOwnProperty]],用来定义新属性时,更新length属性,这个内部属性无法在子类获取,导致子类的length属性行为不正常。
ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。下面是一个继承Array的例子。
class MyArray extends Array {
constructor(...args) {
super(...args);
}
}
var arr = new MyArray();
arr[0] = 12;
arr.length // 1
arr.length = 0;
arr[0] // undefined
extends关键字不仅可以用来继承类,还可以用来继承原生的构造函数。 Array,Map 等内置类也可以扩展。
class MyArray extends Array {
constructor(...args) {
super(...args);
}
}
var arr = new MyArray();
arr[0] = 12;
arr.length // 1
arr.length = 0;
arr[0] // undefined
注意,继承Object的子类,有一个行为差异。
class NewObj extends Object{
constructor(){
super(...arguments);
}
}
var o = new NewObj({attr: true});
o.attr === true // false
上面代码中,NewObj继承了Object,但是无法通过super方法向父类Object传参。这是因为 ES6 改变了Object构造函数的行为,一旦发现Object方法不是通过new Object()这种形式调用,ES6 规定Object构造函数会忽略参数。
4.5 Mixin模式的实现
Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。它的最简单实现如下。
const a = {
a: 'a'
};
const b = {
b: 'b'
};
const c = {...a, ...b}; // {a: 'a', b: 'b’}
上面代码中,c对象是a对象和b对象的合成,具有两者的接口。
下面是一个更完备的实现,将多个类的接口“混入”(mix in)另一个类。
function mix(...mixins) {
class Mix {
constructor() {
for (let mixin of mixins) {
copyProperties(this, new mixin()); // 拷贝实例属性
}
}
}
for (let mixin of mixins) {
copyProperties(Mix, mixin); // 拷贝静态属性
copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性
}
return Mix;
}
function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if ( key !== 'constructor'
&& key !== 'prototype'
&& key !== 'name'
) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
上面代码的mix函数,可以将多个对象合成为一个类。使用的时候,只要继承这个类即可。
class DistributedEdit extends mix(Loggable, Serializable) {
// ...
}
参考资料:
《详解面向对象、构造函数、原型与原型链》 https://segmentfault.com/a/11...
《JavaScript学习笔记-面向对象设计》 https://segmentfault.com/a/11...
《js对象创建方法汇总及对比》 https://segmentfault.com/a/11...
《Class的继承》 http://es6.ruanyifeng.com/#do...
《ES6 Class继承与super》 https://segmentfault.com/a/11...
《ES6类以及继承的实现原理》 https://segmentfault.com/a/11...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。