一篇文章系列
走在前端的大道上
本篇将自己读过的相关 javascript原型和原型链 文章中,对自己有启发的章节片段总结在这(会对原文进行删改),会不断丰富提炼总结更新。
文章——深入理解javascript之原型一般的初学者,在刚刚学习了基本的javascript语法后,都是通过面向函数来编程的。如下代码:
var decimalDigits = 2,
tax = 5;
function add(x, y) {
return x + y;
}
function subtract(x, y) {
return x - y;
}
//alert(add(1, 3));
通过执行各个函数来得到最后的结果。但是利用原型,我们可以优化一些我们的代码,使用构造函数:
首先,函数本体中只存放变量:
var Calculator = function (decimalDigits, tax) {
this.decimalDigits = decimalDigits;
this.tax = tax;
};
其具体的方法通过prototype属性来设置:
Calculator.prototype = {
add: function (x, y) {
return x + y;
},
subtract: function (x, y) {
return x - y;
}
};
//alert((new Calculator()).add(1, 3));
这样就可以通过实例化对象后进行相应的函数操作。这也是一般的js框架采用的方法。
原型还有一个作用就是用来实现继承。首先,定义父对象:
var BaseCalculator = function() {
this.decimalDigits = 2;
};
BaseCalculator.prototype = {
add: function(x, y) {
return x + y;
},
subtract: function(x, y) {
return x - y;
}
};
然后定义子对象,将子对象的原型指向父元素的实例化:
var Calculator = function () {
//为每个实例都声明一个税收数字
this.tax = 5;
};
Calculator.prototype = new BaseCalculator();
我们可以看到Calculator的原型是指向到BaseCalculator的一个实例上,目的是让Calculator集成它的add(x,y)和subtract(x,y)这2个function,还有一点要说的是,由于它的原型是BaseCalculator的一个实例,所以不管你创建多少个Calculator对象实例,他们的原型指向的都是同一个实例
。
上面的代码,运行以后,我们可以看到因为Calculator的原型是指向BaseCalculator的实例上的,所以可以访问他的decimalDigits属性值,那如果我不想让Calculator访问BaseCalculator的构造函数里声明的属性值,那怎么办呢?只需要将Calculator指向BaseCalculator的原型
而不是实例就行了。代码如下:
var Calculator = function () {
this.tax= 5;
};
Calculator.prototype = BaseCalculator.prototype;
在使用第三方库的时候,有时候他们定义的原型方法不能满足我们的需要,我们就可以自己添加一些方法,代码如下:
//覆盖前面Calculator的add() function
Calculator.prototype.add = function (x, y) {
return x + y + this.tax;
};
var calc = new Calculator();
alert(calc.add(1, 1));
原型链
对象的原型指向对象的父,而父的原型又指向父的父,这种原型层层的关系,叫做原型链。
在查找一个对象的属性时,javascript会向上遍历原型链,直到找到给定名称的属性为止,当查找到达原型链的顶部,也即是Object.prototype,仍然没有找到指定的属性,就会返回undefined。
示例如下:
function foo() {
this.add = function (x, y) {
return x + y;
}
}
foo.prototype.add = function (x, y) {
return x + y + 10;
}
Object.prototype.subtract = function (x, y) {
return x - y;
}
var f = new foo();
alert(f.add(1, 2)); //结果是3,而不是13
alert(f.subtract(1, 2)); //结果是-1
我们可以发现,subtrace是按照向上找的原则,而add则出了意外。原因就是,属性在查找的时候是先查找自身的属性,如果没有再查找原型。
说到Object.prototype,就不得不提它的一个方法,hasOwnProperty。它能判断一个对象是否包含自定义属性而不是原型链上的属性,它是javascript中唯一一个处理属性但是不查找原型链的函数。使用代码如下:
// 修改Object.prototype
Object.prototype.bar = 1;
var foo = {goo: undefined};
foo.bar; // 1
'bar' in foo; // true
foo.hasOwnProperty('bar'); // false
foo.hasOwnProperty('goo'); // true
而为了判断prototype对象和某个实例之间的关系,又不得不介绍isPrototyleOf方法,演示如下:
alert(Cat.prototype.isPrototypeOf(cat2)); //true
文章——白话原型和原型链
1. 背景知识
JavaScript和Java、C++等传统面向对象的编程语言不同,它是没有类(class)的概念的(ES6 中的class也只不过是语法糖,并非真正意义上的类),而在JavaScript中,一切皆是对象(object)。在基于类的传统面向对象的编程语言中,对象由类实例化而来,实例化的过程中,类的属性和方法会拷贝到这个对象中;对象的继承实际上是类的继承,在定义子类继承于父类时,子类会将父类的属性和方法拷贝到自身当中。因此,这类语言中,对象创建和继承行为都是通过拷贝完成的。但在JavaScript中,对象的创建、对象的继承(更好的叫法是对象的代理,因为它并不是传统意义上的继承)是不存在拷贝行为的。现在让我们忘掉类、忘掉继承,这一切都不属于JavaScript。
2. 原型和原型链
其实,原型这个名字本身就很容易产生误解,原型在百度词条中的释义是:指原来的类型或模型。按照这个定义解释的话,对象的原型是对象创建自身的模子,模子具备的特点对象都要具有,这俨然就是拷贝的概念。我们已经说过, JavaScript的对象创建不存在拷贝,对象的原型实际上也是一个对象,它和对象本身是完全独立的两个对象。既然如此,原型存在的意义又是什么呢?原型是为了共享多个对象之间的一些共有特性(属性或方法),这个功能也是任何一门面向对象的编程语言必须具备的。A、B两个对象的原型相同,那么它们必然有一些相同的特征。
JavaScript中的对象,都有一个内置属性[[Prototype]],指向这个对象的原型对象。当查找一个属性或方法时,如果在当前对象中找不到定义,会继续在当前对象的原型对象中查找;如果原型对象中依然没有找到,会继续在原型对象的原型中查找(原型也是对象,也有它自己的原型);如此继续,直到找到为止,或者查找到最顶层的原型对象中也没有找到,就结束查找,返回undefined。可以看出,这个查找过程是一个链式的查找,每个对象都有一个到它自身原型对象的链接,这些链接组成的整个链条就是原型链。拥有相同原型的多个对象,他们的共同特征正是通过这种查找模式体现出来的。
在上面的查找过程,我们提到了最顶层的原型对象,这个对象就是Object.prototype,这个对象中保存了最常用的方法,如toString、valueOf、hasOwnProperty等,因此我们才能在任何对象中使用这些方法。
3.创建对象常见的三种方式
1.字面量方式
当通过字面量方式创建对象时,它的原型就是Object.prototype。虽然我们无法直接访问内置属性[[Prototype]],但我们可以通过Object.getPrototypeOf()或对象的proto获取对象的原型。
var obj = {};
Object.getPrototypeOf(obj) === Object.prototype; // true
obj.__proto__ === Object.prototype; // true
2.函数的构造调用
通过函数的构造调用(注意,我们不把它叫做构造函数,因为JavaScript中同样没有构造函数的概念,所有的函数都是平等的,只不过用来创建对象时,函数的调用方式不同而已)也是一种常用的创建对象的方式。基于同一个函数创建出来的对象,理应可以共享一些相同的属性或方法,但这些属性或方法如果放在Object.prototype里,那么所有的对象都可以使用它们了,作用域太大,显然不合适。于是,JavaScript在定义一个函数时,同时为这个函数定义了一个 默认的prototype属性,所有共享的属性或方法,都放到这个属性所指向的对象中。由此看出,通过一个函数的构造调用创建的对象,它的原型就是这个函数的prototype指向的对象。
var f = function(name) { this.name = name };
f.prototype.getName = function() { return this.name; }
//在prototype下存放所有对象的共享方法
var obj = new f('JavaScript');
obj.getName(); // JavaScript
obj.__proto__ === f.prototype; // true
//创建构造函数
function Person(name){
this.name = name
}
//每个构造函数JS引擎都会自动添加一个prototype属性,我们称之为原型,这是一个对象
//每个由构造函数创建的对象都会共享prototype上面的属性与方法
console.log(typeof Person.prototype) // 'object'
//我们为Person.prototype添加sayName方法
Person.prototype.sayName = function(){
console.log(this.name)
}
//创建实例
var person1 = new Person('Messi')
var person2 = new Person('Suarez')
person1.sayName() // 'Messi'
person2.sayName() // 'Suarez'
person1.sayName === person2.sayName //true
我们借助上面的例子来理解构造函数-原型-实例,三者之间的关系,主要有几个基本概念
- 构造函数默认会有一个protoype属性指向它的原型
- 构造函数的原型会有一个consctructor的属性指向构造函数本身, 即
Person.prototype.constructor === Person
- 每一个new出来的实例都有一个隐式的proto属性,指向它们的构造函数的原型,即
person1.__proto__ === Person.prototype
person1.__proto__.constructor === Person
Oject本身是一个构造函数,它也是一个对象,那么
Object.__proto__ === Function.prototype
还有几个需要我们知道的特殊概念:
- Function的原型属性与Function的原型指向同一个对象. 即
- Function.proto == Function.prototype
Object.prototype.__proto__ === null
- typeof Function.prototype === 'function'
3.Object.create()
第三种常用的创建对象的方式是使用Object.create()。这个方法会以你传入的对象作为创建出来的对象的原型。
var obj = {};
var obj2 = Object.create(obj);
obj2.__proto__ === obj; // true
这种方式还可以模拟对象的“继承”行为。
function Foo(name) {
this.name = name;
}
Foo.prototype.myName = function() {
return this.name;
};
function Bar(name,label) {
Foo.call( this, name ); //
this.label = label;
}
// temp对象的原型是Foo.prototype
var temp = Object.create( Foo.prototype );
// 通过new Bar() 创建的对象,其原型是temp, 而temp的原型是Foo.prototype,
// 从而两个原型对象Bar.prototype和Foo.prototype 有了"继承"关系
Bar.prototype = temp;
Bar.prototype.myLabel = function() {
return this.label;
};
var a = new Bar( "a", "obj a" );
a.myName(); // "a"
a.myLabel(); // "obj a"
a.__proto__.__proto__ === Foo.prototype; //true
4. proto和prototype
这是容易混淆的两个属性。proto指向当前对象的原型,prototype是函数才具有的属性,默认情况下,new 一个函数创建出的对象,其原型都指向这个函数的prototype属性。
5. 三种特殊情况
1.对于JavaScript中的内置对象,如String、Number、Array、Object、Function等,因为他们是native代码实现的,他们的原型打印出来都是ƒ () { [native code] }。
2.内置对象本质上也是函数,所以可以通过他们创建对象,创建出的对象的原型指向对应内置对象的prototype属性,最顶层的原型对象依然指向Object.prototype。
'abc'.__proto__ === String.prototype; // true
new String('abc').__proto__ === String.prototype; //true
new Number(1).__proto__ ==== Number.prototype; // true
[1,2,3].__proto__ === Array.prototype; // true
new Array(1,2,3).__proto__ === Array.prototype; // true
({}).__proto__ === Object.prototype; //true
new Object({}).__proto__ === Object.prototype; // true
var f = function() {};
f.__proto__ === Function.prototype; // true
var f = new Function('{}');
f.__proto__ === Function.prototype; // true
3.Object.create(null) 创建出的对象,不存在原型。
var a = Object.create(null);
a.__proto__; // undefined
此外函数的prototype中还有一个constructor方法,建议大家就当它不存在,它的存在让JavaScript原型的概念变得更加混乱,而且这个方法也几乎没有作用。
constructor、proto与prototype在javascript中我们每创建一个对象,该对象都会获得一个__proto__
属性(该属性是个对象),该属性指向创建该对象的构造函数的原型
即prototype
,同时__proto__
对象有一个constructor
属性指向该构造函数。这里我们需要注意的是只有函数才有prototype
,每个对象(函数也是对象)都有__proto__
,Object
本身是个构造函数。举例来说:
var obj = new Object()
// 也可以使用对象字面量创建,但使用Object.create()情况会不一样
// Object本身是个构造函数
Object instanceof Function // true
obj.__proto__ === Object.prototype // true
obj.__proto__.constructor === Object // true
// 我们一般习惯这样写
obj.constructor === Object // true
当我们访问obj.constructor
的时候,obj
本身是没有constructor
属性的,但属性访问会沿着__proto__
向上查找,即在obj.__proto__
里面寻找constructor
属性,如果找到了就返回值,如果未找到则继续向上查找直到obj.__proto__.__proto__...(__proto__) === null
为止,没有找到则返回undefined。这样由__proto__
构成的一条查找属性的线称为‘原型链’。
我们知道JS是单继承的,Object.prototype
是原型链的顶端,所有对象从它继承了包括toString
等等方法和属性。
前面我们说到Object
本身是构造函数,那么它继承了Function.prototype
;Function
也是对象,继承了Object.prototype
。这里就有一个鸡和蛋的问题:
Object instanceof Function // true
Function instanceof Object // true
以下是ES规范的解释:
Function
本身就是函数,Function.__proto__
是标准的内置对象Function.prototype
。
Function.prototype.__proto__
是标准的内置对象Object.prototype
。
function Person(name) {
this.name = name
}
var person1 = new Person('sillywa')
总的来说:先有Object.prototype
(原型链顶端),Function.prototype
继承Object.prototype
而产生,最后,Function
和Object
和其它构造函数继承Function.prototype
而产生。
实例与总结
function Person(name) {
this.name = name
}
var person1 = new Person('sillywa')
person1.__proto__ === Person.prototype
person1.__proto__.__proto__ === Person.prototype.__proto__
person1.__proto__.__proto__ === Object.prototype
Person.prototype.__proto__ === Object.prototype
person1.__proto__.__proto__.__proto__ === null
Person.__proto__ === Function.prototype
以上均返回true,需要注意的是IE浏览器里面并没有实现__proto__
,为了便于理解我们可以这样解释,但是最好不要在实际中使用
Function.prototype.a = 'a';
Object.prototype.b = 'b';
function Person(){};
var p = new Person();
console.log('p.a: '+ p.a); // p.a: undefined
console.log('p.b: '+ p.b); // p.b: b
推荐必读(复习)
下边两张图均来自万物皆空之 JavaScript 原型 文章总结也的非常好