理解JavaScript中的原型和原型链

1、 什么是原型

每一个JavaScript对象(null除外)都和另一个对象相关联。’另一个’对象就是我们熟知的原型,每一个对象都从原型继承属性。

从《JavaScript权威指南》中可以知道,每一个对象都有一个原型对象,那么怎么知道一个对象所对应的原型对象呢?
(ps:在es6中可以使用Object.getPrototypeOf()函数来插件到原型。)
对象__proto__属性的值就是它所对应的原型对象:

1
2
let arr = [];
console.log(arr); // 输出的结果见下图

第二张图里面可以看到,不管我们给数组定义什么内容,它们总是有一些相同的方法和属性比如:map方法,length属性。

上图说明一个对象所拥有的属性不仅仅是它本身拥有的属性,它还会从其他对象中继承一些属性。当js在一个对象中找不到需要的属性时,它会到这个对象的父对象上去找,以此类推,这就构成了对象的原型链
简单来说,在 JavaScript 中每个对象都会有一个 __proto__ 属性,当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么他就会去 __proto__ 里找这个属性,这个 __proto__ 又会有自己的 __proto__,于是就这样一直找下去。

1
2
3
4
5
6
7
8
9
// 来一个栗子深入理解下原型
let person = function() {};
person.prototype.say = function () {
console.log('hello world');
}
let littlePerson = new person();
littlePerson.say(); // hello world

//littlePerson里面是没有定义say方法的,当我们直接调用say方法时,littlePerson里面是获取不到的,于是就去littlePerson里面__proto__里面找,而我们在person.prototype.say里面定义的say方法,于是littlePerson在 person.prototype 中就找到了这个方法。

2、什么是原型链

上文已经提到了原型链,接下来看看《JavaScript权威指南》里面关于原型链的解释:

所有的内置的构造函数(以及大部分的自定义构造函数)都具有一个继承自Object.prototype的原型。例如,Date.prototype的属性继承自Object.prototype,因此由new Date()创建的Date对象同时继承自Date.prototype和Object.prototype。这一系列链接的原型对象就是所谓的原型链(prototype chain)

2.1 继承

假设要查询对象o的属性x,如果o中不存在x,那么将会继续在o的原型对象中查询属性x。如果原型对象中也没有x,但这个原型对象也有原型,那么继续在这个原型对象的原型上执行查询,直到找到x或者查找到一个原型是null的对象为止。可以看到,对象的原型属性构成了一个”链”,通过这个”链”可以实现属性的继承。

下面来一个继承的demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function inherit(p) {
if (p === null) {
throw TypeError(); // 因为原型链终点为null,所以不能输入null
}
if (Object.create) {
return Object.create(p); // Object.create是es6才有的方法,要判断浏览器是否支持该方法
}
// 如果浏览器不支持Object.create就用其他方法来创建新对象
if (typeof(p) !== 'object' && typeof(p) !== 'function') {
throw TypeError();
}
function foo(){};
foo.prototype = p;
return new foo();
}


let obj = {}; // obj从Object.prototype继承对象的方法
obj.x = 1;
let p = inherit(obj); // p继承obj和Object.prototype
p.y = 2;
let q = inherit(p); // q继承p,obj和Object.prototype
console.log(q.x + q.y); // 3

2.2 属性赋值

属性赋值操作首先检查原型链,以此判断是否允许赋值操作。例如,如果o继承自一个只读属性x,那么赋值操作是不允许的。
如果允许属性赋值操作,它也总是在原始对象上创建属性或对已有的属性赋值。而不会去修改原型链。
在JS中,只有在查询属性时才会体会到继承的存在,而设置属性则和继承无关。这是JS的一个重要特征,该特征让程序员可以有选择地覆盖继承的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function inherit(p) {
if (p === null) {
throw TypeError(); // 因为原型链终点为null,所以不能输入null
}
if (Object.create) {
return Object.create(p); // Object.create是es6才有的方法,要判断浏览器是否支持该方法
}
// 如果浏览器不支持Object.create就用其他方法来创建新对象
if (typeof(p) !== 'object' && typeof(p) !== 'function') {
throw TypeError();
}
function foo(){};
foo.prototype = p;
return new foo();
}


let obj = {name: '呆呆'};
let new_obj = inherit(obj);
new_obj.name = '爱死比';
console.log(obj); // {name: "呆呆"}

2.3 属性访问错误

1
2
3
4
5
6
let p = null;
console.log(p.q.length); // Cannot read property 'length' of null


//在不能确定p属性有length属性前,不能这样写,可以试试下面的写法
let len = p && p.q && p.q.length; // 这种写法可以避免类型错误异常

2.4 删除属性

delete运算符只能删除自有属性,不能删除继承属性(要删除继承属性必须从定义这个属性的原型对象上面删除它,而且这会影响到所有继承自这个原型
的对象)当delete表达式删除成功或没有任何副作用(比如删除不存在的属性)时,它返回true,如果delete后不是一个属性访问表达式,delete同样
返回true

1
2
3
4
let p = {name: 'as'};
console.log(p); // {name: "as"}
delete p.name;
console.log(p); // {}

2.5 检测是否存在某个属性

in运算符的左侧是属性名(字符串),右侧是对象。如果对象的自有属性或继承属性中包含这个属性则返回true。

1
2
3
4
let obj = {name: 'aaa'};
console.log('name' in obj); // true , 必须要用字符串,否则不管是否存在都会返回false
console.log(name in obj); // false
console.log('toString' in obj); // true ,obj 继承了 Object.prototype里面的属性

对象的hasOwnProperty()方法用来检测给定的名字是否是对象的自有属性。对于继承属性则返回false。

1
2
3
4
5
let obj = {name: 'aaa'};
console.log(obj);
console.log(obj.hasOwnProperty('name')); // true ,必须要用字符串,否则不管是否存在都会返回false
console.log(obj.hasOwnProperty(name)); // false
console.log(obj.hasOwnProperty('toString')); // false , toString是继承属性

propertyIsEnumerable()是hasOwnProperty()的增强版,只有检测到是自有属性且这个属性的可枚举性(enumerable attribute)为true时才返回true。

1
2
3
4
5
6
let obj = {name: 'aaa'};
console.log(obj);
console.log(obj.propertyIsEnumerable('name')); // true ,obj有一个可枚举的自有属性 name
console.log(obj.propertyIsEnumerable(name)); // false ,不是字符串
console.log(obj.propertyIsEnumerable('toString')); // false , toString是继承属性
console.log(Object.prototype.propertyIsEnumerable('toString')); //false 因为不可枚举

除了使用in运算符之外,另一种更简便的方法就是使用’!==’判断一个属性是否是undefined

1
2
3
4
let obj = {name: 'aaa'};
console.log(obj.name !== undefined); // true
console.log(obj.age !== undefined); // false
console.log(obj.toString !== undefined); // true ,obj继承了Object.prototype里面的toString属性

一个只能用in的特殊案例

1
2
3
4
5
6
7
// in 可以区分不存在的属性和存在但是值为undefined的属性
let obj = {name: undefined};
console.log('name' in obj); //true
console.log(obj.name !== undefined); // false , 属性存在,但是值为undefined
console.log(obj.hasOwnProperty('name')); // true
delete obj.name;
console.log('name' in obj); //false

参考资料:
简单介绍javascript 中__proto__属性的原理
JS重点整理之JS原型链彻底搞清楚
彻底理解js的原型链


理解JavaScript中的原型和原型链
https://xypecho.github.io/2018/05/30/理解JavaScript中的原型和原型链/
作者
很青的青蛙
发布于
2018年5月30日
许可协议