目录
1.构造函数的简单介绍
2.构造函数的缺点
3.prototype属性的作用
4.原型链(prototype chains)
5.constructor属性
5.1:constructor属性的作用
6.instanceof运算符
基于原型链的继承
继承属性
继承方法
使用不同的方法来创建对象和生成原型链
使用语法结构创建的对象
使用构造器创建的对象
使用 Object.create 创建的对象
使用 class 关键字创建的对象
性能
错误实践:扩展原生对象的原型
总结:4 个用于拓展原型链的方法
实现继承的几种方式以及他们的优缺点
1.原型链继承
2.构造函数继承
3.组合继承
4.寄生继承
所谓构造函数,就是提供了一个生成对象的模板并描述对象的基本结构的函数。一个构造函数,可以生成多个对象,每个对象都有相同的结构。总的来说,构造函数就是对象的模板,对象就是构造函数的实例。
构造函数的特点有:
a:构造函数的函数名首字母必须大写。
b:内部使用this对象,来指向将要生成的对象实例。
c:使用new操作符来调用构造函数,并返回对象实例。
看一个最简单的一个例子。
所有的实例对象都可以继承构造函数中的属性和方法。但是,同一个对象实例之间,无法共享属性。
function Person(name,height){
this.name=name;
this.height=height;
this.hobby=function(){
return 'watching movies';
}
}
var boy=new Person('keith',180);
var girl=new Person('rascal',153);
console.log(boy.name); //'keith'
console.log(girl.name); //'rascal'
上面代码中,一个构造函数Person生成了两个对象实例boy和girl,并且有两个属性和一个方法。但是,它们的hobby方法是不一样的。也就是说,每当你使用new来调用构造函数放回一个对象实例的时候,都会创建一个hobby方法。这既没有必要,又浪费资源,因为所有hobby方法都是同样的行为,完全可以被两个对象实例共享。
所以,构造函数的缺点就是:同一个构造函数的对象实例之间无法共享属性或方法。
为了解决构造函数的对象实例之间无法共享属性的缺点,js提供了prototype属性。
js中每个数据类型都是对象(除了null和undefined),而每个对象都继承自另外一个对象,后者称为“原型”(prototype)对象,只有null除外,它没有自己的原型对象。
原型对象上的所有属性和方法,都会被对象实例所共享。
通过构造函数生成对象实例时,会将对象实例的原型指向构造函数的prototype属性。每一个构造函数都有一个prototype属性,这个属性就是对象实例的原型对象。
function Person(name,height){
this.name=name;
this.height=height;
}
Person.prototype.hobby=function(){
return 'watching movies';
}
var boy=new Person('keith',180);
var girl=new Person('rascal',153);
console.log(boy.name); //'keith'
console.log(girl.name); //'rascal'
console.log(boy.hobby===girl.hobby); //true
如果将hobby方法放在原型对象上,那么两个实例对象都共享着同一个方法。我希望大家都能理解的是,对于构造函数来说,prototype是作为构造函数的属性;对于对象实例来说,prototype是对象实例的原型对象。所以prototype即是属性,又是对象。
原型对象的属性不是对象实例的属性。对象实例的属性是继承自构造函数定义的属性,因为构造函数内部有一个this关键字来指向将要生成的对象实例。对象实例的属性,其实就是构造函数内部定义的属性。只要修改原型对象上的属性和方法,变动就会立刻体现在所有对象实例上。
上面的代码,当修改了原型对象的hobby方法之后,两个对象实例都发生了变化。这是因为对象实例其实是没有hobby方法,都是读取原型对象的hobby方法。也就是说,当某个对象实例没有该属性和方法时,就会到原型对象上去查找。如果实例对象自身有某个属性或方法,就不会去原型对象上查找。
boy.hobby=function(){
return 'play basketball';
}
console.log(boy.hobby()); //'play basketball'
console.log(girl.hobby()); //'swimming'
上面代码中,boy对象实例的hobby方法修改时,就不会在继承原型对象上的hobby方法了。不过girl仍然会继承原型对象的方法。
对象的属性和方法,有可能是定义在自身,也有可能是定义在它的原型对象。由于原型对象本身对于对象实例来说也是对象,它也有自己的原型,所以形成了一条原型链(prototype chain)。比如,a对象是b对象的原型,b对象是c对象的原型,以此类推。所有一切的对象的原型顶端,都是Object.prototype,即Object构造函数的prototype属性指向的那个对象。
当然,Object.prototype对象也有自己的原型对象,那就是没有任何属性和方法的null对象,而null对象没有自己的原型。
1 console.log(Object.getPrototypeOf(Object.prototype)); //null
2 console.log(Person.prototype.isPrototypeOf(boy)) //true
原型链(prototype chain)的特点有:
a:读取对象的某个属性时,JavaScript引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的Object.prototype还是找不到,则返回undefined。
b:如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做“覆盖”(overiding)。
c:一级级向上在原型链寻找某个属性,对性能是有影响的。所寻找的属性在越上层的原型对象,对性能的影响越大。如果寻找某个不存在的属性,将会遍历整个原型链。
length属性是继承自Array.prototype的,属于原型对象上的一个属性。join方法也是继承自Array.prototype的,属于原型对象上的一个方法。这两个方法是所有数组所共享的。当实例对象上没有这个length属性时,就会去原型对象查找。
valueOf方法是继承自Object.prototype的。首先,arr数组是没有valueOf方法的,所以就到原型对象Array.prototype查找。然后,发现Array.prototype对象上没有valueOf方法。最后,再到它的原型对象Object.prototype查找。
来看看Array.prototype对象和Object.prototype对象分别有什么属性和方法。
console.log(Object.getOwnPropertyNames(Array.prototype))
//["length", "toSource", "toString", "toLocaleString", "join", "reverse", "sort", "push", "pop", "shift", "unshift", "splice", "concat", "slice", "lastIndexOf", "indexOf", "forEach", "map", "filter", "reduce", "reduceRight", "some", "every", "find", "findIndex", "copyWithin", "fill", "entries", "keys", "values", "includes", "constructor", "$set", "$remove"]
console.log(Object.getOwnPropertyNames(Object.prototype))
// ["toSource", "toString", "toLocaleString", "valueOf", "watch", "unwatch", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "__defineGetter__", "__defineSetter__", "__lookupGetter__", "__lookupSetter__", "__proto__", "constructor"]
prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数。
要注意的是,prototype是构造函数的属性,而constructor则是构造函数的prototype属性所指向的那个对象,也就是原型对象的属性。注意不要混淆。
由于constructor属性是定义在原型(prototype)对象上面,意味着可以被所有实例对象继承。
function A(){};
var a=new A();
console.log(a.constructor); //A()
console.log(a.constructor===A.prototype.constructor);//true
上面代码中,a是构造函数A的实例对象,但是a自身没有contructor属性,该属性其实是读取原型链上面的。
a:分辨原型对象到底属于哪个构造函数
1 2 3 4 |
|
上面代码表示,使用constructor属性,确定实例对象a的构造函数是A,而不是Array。
b:从实例新建另一个实例
1 2 3 4 |
|
上面代码中,a是构造函数A的实例对象,可以从a.constructor间接调用构造函数。
c:调用自身的构造函数成为可能
1 2 3 |
|
d:提供了一种从构造函数继承另外一种构造函数的模式
1 2 3 4 5 |
|
上面代码中,Father和Son都是构造函数,在Son内部的this上调用Father,就会形成Son继承Father的效果。
e:由于constructor属性是一种原型对象和构造函数的关系,所以在修改原型对象的时候,一定要注意constructor的指向问题。
解决方法有两种,要么将constructor属性指向原来的构造函数,要么只在原型对象上添加属性和方法,避免instanceof失真。
instanceof运算符返回一个布尔值,表示指定对象是否为某个构造函数的实例。
因为instanceof对整个原型链上的对象都有效,所以同一个实例对象,可能会对多个构造函数都返回true。
注意,instanceof对象只能用于复杂数据类型(数组,对象等),不能用于简单数据类型(布尔值,数字,字符串等)。
此外,null和undefined都不是对象,所以instanceof 总是返回false。
利用instanceof运算符,还可以巧妙地解决,调用构造函数时,忘了加new命令的问题。
instanceof运算符来判断函数体内的this关键字是否指向构造函数Keith的实例,如果不是,就表明忘记加new命令,此时构造函数会返回一个对象实例,避免出现意想不到的结果。
JavaScript 对象是动态的属性“包”(指其自己的属性)。JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
遵循ECMAScript标准,someObject.[[Prototype]]
符号是用于指向 someObject
的原型。从 ECMAScript 6 开始,[[Prototype]]
可以通过 Object.getPrototypeOf()
和 Object.setPrototypeOf()
访问器来访问。这个等同于 JavaScript 的非标准但许多浏览器实现的属性 __proto__
。
但它不应该与构造函数 func
的 prototype
属性相混淆。被构造函数创建的实例对象的 [[prototype]]
指向 func
的 prototype
属性。Object.prototype
属性表示 Object
的原型对象。
这里演示当尝试访问属性时会发生什么:
// 不要在 f 函数的原型上直接定义 f.prototype = {b:3,c:4};这样会直接打破原型链
// o.[[Prototype]] 有属性 b 和 c
// (其实就是 o.__proto__ 或者 o.constructor.prototype)
// o.[[Prototype]].[[Prototype]] 是 Object.prototype.
// 最后o.[[Prototype]].[[Prototype]].[[Prototype]]是null
// 让我们从一个自身拥有属性a和b的函数里创建一个对象o:
let f = function () {
this.a = 1;
this.b = 2;
}
/* 这么写也一样
function f() {
this.a = 1;
this.b = 2;
}
*/
let o = new f(); // {a: 1, b: 2}
// 在f函数的原型上定义属性
f.prototype.b = 3;
f.prototype.c = 4;
// 不要在 f 函数的原型上直接定义 f.prototype = {b:3,c:4};这样会直接打破原型链
// o.[[Prototype]] 有属性 b 和 c
// (其实就是 o.__proto__ 或者 o.constructor.prototype)
// o.[[Prototype]].[[Prototype]] 是 Object.prototype.
// 最后o.[[Prototype]].[[Prototype]].[[Prototype]]是null
// 这就是原型链的末尾,即 null,
// 根据定义,null 就是没有 [[Prototype]]。
// 综上,整个原型链如下:
// {a:1, b:2} ---> {b:3, c:4} ---> Object.prototype---> null
console.log(o.a); // 1
// a是o的自身属性吗?是的,该属性的值为 1
console.log(o.b); // 2
// b是o的自身属性吗?是的,该属性的值为 2
// 原型上也有一个'b'属性,但是它不会被访问到。
// 这种情况被称为"属性遮蔽 (property shadowing)"
console.log(o.c); // 4
// c是o的自身属性吗?不是,那看看它的原型上有没有
// c是o.[[Prototype]]的属性吗?是的,该属性的值为 4
console.log(o.d); // undefined
// d 是 o 的自身属性吗?不是,那看看它的原型上有没有
// d 是 o.[[Prototype]] 的属性吗?不是,那看看它的原型上有没有
// o.[[Prototype]].[[Prototype]] 为 null,停止搜索
// 找不到 d 属性,返回 undefined
JavaScript 并没有其他基于类的语言所定义的“方法”。在 JavaScript 里,任何函数都可以添加到对象上作为对象的属性。函数的继承与其他的属性继承没有差别,包括上面的“属性遮蔽”(这种情况相当于其他语言的方法重写)。
当继承的函数被调用时,this 指向的是当前继承的对象,而不是继承的函数所在的原型对象
var o = {
a: 2,
m: function(){
return this.a + 1;
}
};
console.log(o.m()); // 3
// 当调用 o.m 时,'this' 指向了 o.
var p = Object.create(o);
// p是一个继承自 o 的对象
p.a = 4; // 创建 p 的自身属性 'a'
console.log(p.m()); // 5
// 调用 p.m 时,'this' 指向了 p
// 又因为 p 继承了 o 的 m 函数
// 所以,此时的 'this.a' 即 p.a,就是 p 的自身属性 'a'
在控制台显示的JavaScript代码块中,我们可以看到doSomething函数的一个默认属性prototype。而这段代码运行之后,控制台应该显示类似如下的结果:
{
constructor: ƒ doSomething(),
__proto__: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf()
}
}
如上所示, doSomeInstancing
中的__proto__
是 doSomething.prototype
. 但这是做什么的呢?当你访问doSomeInstancing
中的一个属性,浏览器首先会查看doSomeInstancing
中是否存在这个属性。
如果 doSomeInstancing
不包含属性信息, 那么浏览器会在 doSomeInstancing
的 __proto__
中进行查找(同 doSomething.prototype). 如属性在 doSomeInstancing
的 __proto__
中查找到,则使用 doSomeInstancing
中 __proto__
的属性。
否则,如果 doSomeInstancing
中 __proto__
不具有该属性,则检查doSomeInstancing
的 __proto__
的 __proto__
是否具有该属性。默认情况下,任何函数的原型属性 __proto__
都是 window.Object.prototype.
因此, 通过doSomeInstancing
的 __proto__
的 __proto__
( 同 doSomething.prototype 的 __proto__
(同 Object.prototype
)) 来查找要搜索的属性。
如果属性不存在
doSomeInstancing
的 __proto__
的 __proto__
中, 那么就会在doSomeInstancing
的 __proto__
的 __proto__
的 __proto__
中查找。然而, 这里存在个问题:doSomeInstancing
的 __proto__
的 __proto__
的 __proto__
其实不存在。因此,只有这样,在 __proto__
的整个原型链被查看之后,这里没有更多的 __proto__
, 浏览器断言该属性不存在,并给出属性值为 undefined
的结论。
让我们在控制台窗口中输入更多的代码,如下:
function doSomething(){}
doSomething.prototype.foo = "bar";
var doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value";
console.log("doSomeInstancing.prop: " + doSomeInstancing.prop);
console.log("doSomeInstancing.foo: " + doSomeInstancing.foo);
console.log("doSomething.prop: " + doSomething.prop);
console.log("doSomething.foo: " + doSomething.foo);
console.log("doSomething.prototype.prop: " + doSomething.prototype.prop);
console.log("doSomething.prototype.foo: " + doSomething.prototype.foo);
结果如下:
doSomeInstancing.prop: some value
doSomeInstancing.foo: bar
doSomething.prop: undefined
doSomething.foo: undefined
doSomething.prototype.prop: undefined
doSomething.prototype.foo: bar
var o = {a: 1};
// o 这个对象继承了 Object.prototype 上面的所有属性
// o 自身没有名为 hasOwnProperty 的属性
// hasOwnProperty 是 Object.prototype 的属性
// 因此 o 继承了 Object.prototype 的 hasOwnProperty
// Object.prototype 的原型为 null
// 原型链如下:
// o ---> Object.prototype ---> null
var a = ["yo", "whadup", "?"];
// 数组都继承于 Array.prototype
// (Array.prototype 中包含 indexOf, forEach 等方法)
// 原型链如下:
// a ---> Array.prototype ---> Object.prototype ---> null
function f(){
return 2;
}
// 函数都继承于 Function.prototype
// (Function.prototype 中包含 call, bind等方法)
// 原型链如下:
// f ---> Function.prototype ---> Object.prototype ---> null
在 JavaScript 中,构造器其实就是一个普通的函数。当使用 new 操作符 来作用这个函数时,它就可以被称为构造方法(构造函数)。
function Graph() {
this.vertices = [];
this.edges = [];
}
Graph.prototype = {
addVertex: function(v){
this.vertices.push(v);
}
};
var g = new Graph();
// g 是生成的对象,他的自身属性有 'vertices' 和 'edges'。
// 在 g 被实例化时,g.[[Prototype]] 指向了 Graph.prototype。
Object.create
创建的对象ECMAScript 5 中引入了一个新方法:Object.create()
。可以调用这个方法来创建一个新对象。新对象的原型就是调用 create 方法时传入的第一个参数:
var a = {a: 1};
// a ---> Object.prototype ---> null
var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (继承而来)
var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null
var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty); // undefined, 因为d没有继承Object.prototype
class
关键字创建的对象ECMAScript6 引入了一套新的关键字用来实现 class。使用基于类语言的开发人员会对这些结构感到熟悉,但它们是不同的。JavaScript 仍然基于原型。这些新的关键字包括 class
, constructor
,static
,extends
和 super
。
"use strict";
class Polygon {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
class Square extends Polygon {
constructor(sideLength) {
super(sideLength, sideLength);
}
get area() {
return this.height * this.width;
}
set sideLength(newLength) {
this.height = newLength;
this.width = newLength;
}
}
var square = new Square(2);
在原型链上查找属性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。另外,试图访问不存在的属性时会遍历整个原型链。
遍历对象的属性时,原型链上的每个可枚举属性都会被枚举出来。要检查对象是否具有自己定义的属性,而不是其原型链上的某个属性,则必须使用所有对象从 Object.prototype
继承的 hasOwnProperty
方法。下面给出一个具体的例子来说明它:
console.log(g.hasOwnProperty('vertices'));
// true
console.log(g.hasOwnProperty('nope'));
// false
console.log(g.hasOwnProperty('addVertex'));
// false
console.log(g.__proto__.hasOwnProperty('addVertex'));
// true
hasOwnProperty
是 JavaScript 中唯一一个处理属性并且不会遍历原型链的方法。(译者注:原文如此。另一种这样的方法:Object.keys()
)
注意:检查属性是否为 undefined
是不能够检查其是否存在的。该属性可能已存在,但其值恰好被设置成了 undefined
。
经常使用的一个错误实践是扩展 Object.prototype
或其他内置原型。
这种技术被称为猴子补丁并且会破坏封装。尽管一些流行的框架(如 Prototype.js)在使用该技术,但仍然没有足够好的理由使用附加的非标准方法来混入内置原型。
扩展内置原型的唯一理由是支持 JavaScript 引擎的新特性,如 Array.forEach
。
下面列举四种用于拓展原型链的方法,以及他们的优势和缺陷。下列四个例子都创建了完全相同的 inst
对象(所以在控制台上的输出也是一致的),为了举例,唯一的区别是他们的创建方法不同。
名称 | 例子 | 优势 | 缺陷 |
New-initialization | |
支持目前以及所有可想象到的浏览器(IE5.5都可以使用). 这种方法非常快,非常符合标准,并且充分利用JIST优化。 | 为使用此方法,这个问题中的函数必须要被初始化。 在这个初始化过程中,构造可以存储一个唯一的信息,并强制在每个对象中生成。但是,这个一次性生成的独特信息,可能会带来潜在的问题。另外,构造函数的初始化,可能会给生成对象带来并不想要的方法。 然而,如果你只在自己的代码中使用,你也清楚(或有通过注释等写明)各段代码在做什么,这些在大体上都根本不是问题(事实上,还常常是有益处的)。 |
Object.create | |
支持当前所有非微软版本或者 IE9 以上版本的浏览器。允许一次性地直接设置 __proto__ 属性,以便浏览器能更好地优化对象。同时允许通过 Object.create(null) 来创建一个没有原型的对象。 |
不支持 IE8 以下的版本。然而,随着微软不再对系统中运行的旧版本浏览器提供支持,这将不是在大多数应用中的主要问题。 另外,这个慢对象初始化在使用第二个参数的时候有可能成为一个性能黑洞,因为每个对象的描述符属性都有自己的描述对象。当以对象的格式处理成百上千的对象描述的时候,可能会造成严重的性能问题。 |
Object.setPrototypeOf |
|
支持所有现代浏览器和微软IE9+浏览器。允许动态操作对象的原型,甚至能强制给通过 Object.create(null) 创建出来的没有原型的对象添加一个原型。 |
这个方式表现并不好,应该被弃用。如果你在生产环境中使用这个方法,那么快速运行 Javascript 就是不可能的,因为许多浏览器优化了原型,尝试在调用实例之前猜测方法在内存中的位置,但是动态设置原型干扰了所有的优化,甚至可能使浏览器为了运行成功,使用完全未经优化的代码进行重编译。 不支持 IE8 及以下的浏览器版本。 |
__proto__ | |
支持所有现代非微软版本以及 IE11 以上版本的浏览器。将 __proto__ 设置为非对象的值会静默失败,并不会抛出错误。 |
应该完全将其抛弃因为这个行为完全不具备性能可言。 如果你在生产环境中使用这个方法,那么快速运行 Javascript 就是不可能的,因为许多浏览器优化了原型,尝试在调用实例之前猜测方法在内存中的位置,但是动态设置原型干扰了所有的优化,甚至可能使浏览器为了运行成功,使用完全未经优化的代码进行重编译。不支持 IE10 及以下的浏览器版本。 |
要搞懂JS继承,我们首先要理解原型链:每一个实例对象都有一个__proto__属性(隐式原型),在js内部用来查找原型链;每一个构造函数都有prototype属性(显示原型),用来显示修改对象的原型,实例.__proto__=构造函数.prototype=原型。原型链的特点就是:通过实例.__proto__查找原型上的属性,从子类一直向上查找对象原型的属性,继而形成一个查找链即原型链。
我们使用原型继承时,主要利用sub.prototype=new super,这样连通了子类-子类原型-父类。
//父类,带属性
function Super(){
this.flag = true;
}
//为了提高复用性,方法绑定在父类原型属性上
Super.prototype.getFlag = function(){
return this.flag;
}
//来个子类
function Sub(){
this.subFlag = false;
}
//实现继承
Sub.prototype = new Super; //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//给子类添加子类特有的方法,注意顺序要在继承之后
Sub.prototype.getSubFlag = function(){
return this.subFlag;
}
//构造实例
var es5 = new Sub;
缺点:构造函数原型上的属性在所有该构造函数构造的实例上是共享的,即属性没有私有化,原型上属性的改变会作用到所有的实例上。
在构造子类构造函数时内部使用call或apply来调用父类的构造函数
function Super(){
this.flag = true;
}
function Sub(){
Super.call(this) //如果父类可以需要接收参数,这里也可以直接传递
}
var obj = new Sub();
obj.flag = false;
var obj_2 = new Sub();
console.log(obj.flag) //结果是false,因为被修改了
console.log(obj_2.flag) //依然是true,不会相互影响
优缺点:实现了属性的私有化,但是子类无法访问父类原型上的属性。
利用构造函数和原型链的方法,可以比较完美的实现继承
function Super(){
this.flag = true;
}
Super.prototype.getFlag = function(){
return this.flag; //继承方法
}
function Sub(){
this.subFlag = false;
Super.call(this) //继承属性
}
Sub.prototype = new Super;
var obj = new Sub();
Sub.prototype.constructor = Sub;
Super.prototype.getSubFlag = function(){
return this.flag;
}
注:
这里还有个小问题,Sub.prototype = new Super; 会导致Sub.prototype的constructor指向Super;然而constructor的定义是要指向原型属性对应的构造函数的,Sub.prototype是Sub构造函数的原型,所以应该添加一句纠正:Sub.prototype.constructor = Sub;
即将sub.prototype=new super改为sub.prototype=Object.create(supper.prototype),避免了组合继承中构造函数调用了两次的弊端。