最近在看underscore源码,涉及到js原型相关的知识,因而重温了一遍,在此作下记录。html
js原型是其语法的一个难点,也是一个重点,要深刻学习js必须掌握的点。要想读懂别人的框架和库,了解这些基础知识是必不可少的。 js原型主要为了提取公共属性和方法,实现对象属性和方法的继承。说到原型,可能就有几个相关的词:prototype、__proto__、constructor、instanceof。下面经过这几个关键词来说解原型。数组
说到原型,先说一个概念,js里函数(function)是一种特殊的对象,有个说法是js里一切都是对象,这个说法不正确,要排除一些特殊类型,如:undefined, null (虽然null的typeof, toString返回类型是object,历史遗留bug)。浏览器
js里全部的对象都有__proto__属性,能够称为隐式原型,这个属性在低版本ie浏览器中没法读取到。一个对象的隐式原型指向构造该对象的构造函数的原型,使得该对象可以继承其构造函数原型上的属性和方法。框架
函数对象除了具备__proto__这个属性外,还有一个专有属性prototype。这个属性指向一个对象(命名a对象),从该函数实例化的对象(命名b对象)。b对象能共享a对象的全部属性和方法。反过来a对象的constructor属性又指向这个函数。函数
constructor 属性是专门为 function 而设计的,它存在于每个 function 对象的prototype 属性中。这个 constructor 保存了指向 function 的一个引用。学习
constructor和prototype被设计时是构造函数和原型间相互指向,能够当作互逆的,因为实例继承了prototype对象上的属性(包括constructor),故实例的constructor也是指向构造函数。虽然他们俩是互逆,可是二者没有必然联系,修改其中一个的指向,另外一个并不会变。因此在js中经过原型来继承时,通常替换原型时,会附带替换掉constructor的指向。测试
// constructor与prototype
Object.prototype === Object.prototype
Object.prototype.constructor === Object
// 实例指向构造函数
var a = new Object({});
a.constructor === Object;
// 修改一个指向
function a() {}
a.prototype = {
say: function () {
console.log('hello world');
}
}
a.prototype.constructor === Object //true
// 由于a.prototype从新赋值时,直接是赋值的一个对象,
// 这个对象没有经过构造函数来生成,默认就会以new Object方式。故构造函数就是Object。
// 因此通常要手动从新指向构造函数
a.prototype.constructor = a;
复制代码
constructor设计初是被用来判断对象类型的,因为其易变性,通常不使用它来作判断,使用instanceof来替代它。ui
instanceof运算符,它用来判断一个构造函数的prototype属性所指向的对象是否存在另一个要检测对象的原型链上。通常用来判断一个实例是否从一个构造函数实例化过来,用一个函数模拟instanceof函数:this
function _instanceof(A, B) {
var O = B.prototype;
A = A.__proto__;
while (true) {
if (A === null) // 循环查找原型链,一直到Object.prototype.__proto__ = null
return false; // 退出循环
if (O === A)
return true;
A = A.__proto__;
}
}
复制代码
说了这么可能是不是以为有点绕,拿出个人杀手锏,祭出我收藏的一张图,该图很清晰的解释了这些关系。看了这张图后,瞬间理清了原型,废话很少说,上图:spa
看了上面的图后相信整个原型比较清晰了,下面说说整个原型中几个特殊对象。
js里的内置对象Object、Array、Function、Date、Math、Number、String、Boolean、RegExp等都是构造函数对象,能够经过new实例化对象出来。其__proto__属性都指向Function.prototype。Function这个特殊对象,是上面其余函数对象的构造函数。
这里有一条链,以Array为例:
js中上面写的这些对象能够当作是从Function构造函数new出来的对象(实例),只不过与Object,Array构造函数 new出来的对象有点不一样,实例化出来的对象是函数对象。因此有如下等式成立。
Array.__proto__ === Function.prototype // true
Object.__proto__ === Function.prototype // true
Array.constructor === Function // true
Object.constructor === Function // true
复制代码
因为实例化的Array、Object等属于函数对象,它就有prototype属性,故给每一个函数对象配了个原型,如:Array.prototype、Object.prototype,从Array、Object等实例化的对象能够完成一些相同的功能,故给这些对象内置了不少方法,让全部实例化的对象都具有这些方法,故在原型上挂载了不少方法。好比Array.prototype的方法:push、shift、unshift、concat等。 还有个特例以下:
Function.__proto__ === Function.prototype
复制代码
Function 这个函数既能够当作构造函数,也能够当作实例后的函数对象。
不论是构造函数、原型、仍是实例化对象,其都属于对象,对象的原型最初来源都是Object.prototype这个原型对象,故:
Function.prototype.__proto__ === Object.prototype
Array.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null
复制代码
而Object.prototype这个对象的__proto__属性就为null了。 最后上一张我本身画的关于原型的图:
既然说到了原型链,来讲一下几个相关属性,hasOwnProperty、isPrototypeOf、in。这几个属性在原型概念中常常用到。
js的原型主要实现了属性和方法的继承,既然有继承,属性和方法就有本身的和继承来的之分。那么怎么去区分呢?
var a = {name: 'li'};
a.hasOwnProperty('hasOwnProperty'); // false
a.hasOwnProperty('name'); // true
复制代码
hasOwnProperty属性继承自Object.prototype,故返回false, name则是建立时,自带的,故返回false
var o = {}
var a = function () {}
var b = new a();
a.prototype = o;
var c = new a();
o.isPrototypeOf(b); // false
o.isPrototypeOf(c); // true
复制代码
与hasOwnProperty不一样的是,它会遍历该对象上全部可枚举属性,包括原型链上的属性,有就返回true,没有就返回false;
function a() {
this.name = 'li'
}
a.prototype = {age: 20};
var b = new a();
for (var key in b) {
if (b.hasOwnProperty(key)) {
console.log('自身属性'+ key);
} else {
console.log('继承属性'+ key);
}
}
上面的方法常常区分一个对象中的属性是自身属性仍是继承属性。
复制代码
for...in 循环只遍历可枚举属性,使用内置构造函数,像 Array、Object、Number、Boolean、String等构造函数的原型上的属性都不可枚举。 如:Object.prototype.toString方法。 固然若是toString方法被重写,仍是能够遍历的,如:
function animal() {
this.name = 'lilei'
}
animal.prototype.toString = function () {
console.log('animal say');
}
var cat = new animal();
for (var key in cat) {
console.log(key); // name, toString
}
复制代码
可是在 IE < 9 浏览器中(万恶的 IE),Object、Array等构造函数的原型上的属性即便被重写了,仍是不能被枚举到。
(1)、说到可枚举,你可能想到了一个函数,没错就是propertyIsEnumerable函数,他是Object.prototype上的一个方法,他能检测一个属性在其对象上是否能够枚举。 该方法只能检测对象的自有属性,对于其原型链上的属性始终返回false,这一点要与for ... in 中的可枚举区分开。
function a() {
this.name = 'liming';
}
a.prototype.say = function () {
console.log(1);
}
var b = new a();
b.propertyIsEnumerable('name') // true
b.propertyIsEnumerable('say') // false
复制代码
(2)、对象的属性分为可枚举和不可枚举之分,说了这么多,其实它们是由属性的enumerable值决定的。如经过Object.defineProperty函数建立的属性,能够添加该字段来决定该属性是否可枚举。
var a = {name: 'xiao ming'}
Object.defineProperty(a, "gender", {
value: "male",
enumerable: false
});
a.propertyIsEnumerable('name') // true
a.propertyIsEnumerable('gender') // false
复制代码
(3)、到此应该已经结束了,可是我仍是想提到一个函数,Object.keys。该函数返回一个对象的key的数组。看个例子
function q() {
this.name = 'lilei'
}
q.prototype.say = function () {
console.log('say');
}
var a = new q();
Object.defineProperty(a, "gender", {
value: "male",
enumerable: false
});
Object.keys(a) // ['name']
复制代码
说明该方法返回该对象自有的可枚举属性。
(4)、我还想说一下JSON.stringify方法,咱们都只到JSON.stringify方法能够序列化对象。你有经过它克隆对象没?
var a = {name: 'liming'}
var b = JSON.parse(JSON.stringify(a)) // {name: 'liming'}
复制代码
没错对于简单的对象,咱们能够这样克隆,可是他能保存对象里的全部属性吗? 咱们来看一下:
function f() {
this.name = 'lilei';
this.like= undefined;
this.say = function () {}
}
f.prototype.age = 20;
var a = new f();
Object.defineProperty(a, "gender", {
value: "male",
enumerable: false
});
var b = JSON.parse(JSON.stringify(a)) // {name: 'lilei'}
复制代码
显然该方法并不能保存对象里全部属性,事实上stringify只能保存该对象本身的可枚举属性,不能保存其原型上的属性,而且本身的属性也必须知足如下要求: 一、stringify只能保存基础类型的:数字、字符串、布尔值、null四种,不支持undefined。 二、stringify方法不支持函数; 三、除了RegExp、Error对象,JSON语法支持其余全部对象; 关于其详细内容,请看这篇文章传送门
你可能感受文章后面说了一堆方法好像跟原型没多大关联,确实关联性不是很大,可是它们方法内部都涉及到了对象的属性遍历,对象属性遍历天然就联系到原型链上的属性是否可遍历,属性的可枚举性等一系列概念,因此就把它们都提了一下。
本人能力有限,以上内容为我的理解,若有错误,欢迎指正。