什么是原型?
简单来说,可以通过
new
、function
、语法结构
来实例一个对象的,就可以称呼为原型。
// 例如
const arr = new Array(); // Array是原型,arr是实例对象
const obj = new Object(); // Object是原型,obj是实例对象
const Fn = function(){}; // function关键字,Function是原型,Fn是实例对象
const arr1 = []; // 语法结构,原型是Array
const obj = {}; // 语法结构,原型是Object
- javascript内置原型有:Object、Array、Function、Map、Set、Number、String、Boolean
什么是原型链?
- 每个实例对象( object )都有一个私有属性(称之为
__proto__
)指向它的构造函数的原型对象(prototype
)。该原型对象也有一个自己的原型对象(__proto__
) ,层层向上直到一个对象的原型对象为null
。根据定义,null
没有原型,并作为这个原型链中的最后一个环节。- 几乎所有 JavaScript 中的对象都是位于原型链顶端的
Object
的实例。- 以上引用于《MDN-继承与原型链》
PS:上面的描述看不懂没关系,看下面的就行了
首先我们先明确 3 个概念:
- 原型是指:Object、Array、Function……等等
- 原型对象是指:Object.prototype、Array.prototype、Function.prototype……等等
- 每个实例对象有一个
__proto__
属性会指向对应的原型对象
比如:
const obj = {}; // obj是实例对象。其原型是Object
obj.__proto__ === Object.prototype; // true
一条完整原型链的构成:
- 原型链:实例对象 —>
__proto__
—> 原型对象 —> …… —>__proto__
—> Object原型对象 —>__proto__
—> null
请看以下例子:
Object原型链:实例对象obj —>
__proto__
—>Object
原型对象 —>__proto__
—> null
const obj = {};
console.log('obj的爸爸原型对象 === Object原型对象?',obj.__proto__ === Object.prototype);
console.log('obj的爷爷原型对象',obj.__proto__.__proto__);
// 打印结果:
// obj的爸爸原型对象 === Object原型对象? true
// obj的爷爷原型对象 null
数组原型链:实例对象arr —>
__proto__
—>Array
原型对象 —>__proto__
—>Object
原型对象 —>__proto__
—> null
const arr = [];
console.log('arr的爸爸原型对象 === Array原型对象?',arr.__proto__ === Array.prototype);
console.log('arr的爷爷原型对象 === Object原型对象?',arr.__proto__.__proto__ === Object.prototype);
// 打印输出结果:
// arr的爸爸原型对象 === Array原型对象? true
// arr的爷爷原型对象 === Object原型对象? true
普通函数原型链:实例对象fn—>
__proto__
—>Function
原型对象 —>__proto__
—>Object
原型对象 —>__proto__
—> null
const fn = function(){};
console.log('fn的爸爸原型对象 === Function原型对象?',fn.__proto__ === Function.prototype);
console.log('fn的爷爷原型对象 === Object原型对象?',fn.__proto__.__proto__ === Object.prototype);
// 打印输出结果:
// fn的爸爸原型对象 === Function原型对象? true
// fn的爷爷原型对象 === Object原型对象? true
构造函数原型链:实例对象newFn—>
__proto__
—> Fn原型对象 —>__proto__
—> Object原型对象 —>__proto__
—> null
const Fn = function(){};
const newFn = new Fn();
console.log('newFn的爸爸原型对象 === Function原型对象?',newFn.__proto__ === Function.prototype);
console.log('newFn的爸爸原型对象 === Fn原型对象?',newFn.__proto__ === Fn.prototype);
console.log('newFn的爷爷原型对象 === Object原型对象?',fn.__proto__.__proto__ === Object.prototype);
// 打印输出结果:
// newFn的爸爸原型对象 === Function原型对象? false
// newFn的爸爸原型对象 === Fn原型对象? true
// newFn的爷爷原型对象 === Object原型对象? true
PS:构造函数其实就是一个普通的函数,当通过new去实例对象时,普通函数就可以称呼为构造函数。
以上例子可以看得出来:
- Object原型对象都是处于几条原型链的最终环,而且Object原型对象的
__proto__
已经是null了,代表了没有再上一级原型了。- 这意味着javascript中Object原型对象处于原型链顶端。
PS:其他的例子我就不列出来了,建议各位童鞋可以自己去实践一下。
原型链的特点
javascript中当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。《MDN-继承与原型链-基于原型链的继承》
下面我们来看相关的例子:
const Product = function(){}
const keyboard = new Product();
console.log(keyboard.name); // undefined
Product.prototype.name = '键盘';// 增加Product的原型对象属性
console.log(keyboard.name); // 键盘
可以看到原先
keyboard.name
是undefined
的,因为实例对象keyboard
本身并不存在name
属性。
而当我们为keyboard
的原型对象Product.prototype
增加name
属性后,keyboard.name
就有值了。
可见当访问对象一个属性的时候,当对象本身不存在此属性时,则会向上级原型搜索。
那么根据这个特性,我突然想到一个问题,关于 for in 遍历,接下来实践一下:
const Product = function(){
this.name = '键盘';
this.price = 99.9;
}
Product.prototype.weight = '500克';
const keyboard = new Product();
for(let key in keyboard){
console.log(key, keyboard[key]);
}
// 打印结果:
// name 键盘
// price 99.9
// weight 500克
由此可以判断,使用 for in 遍历对象属性时,除了会把对象本身可枚举属性进行遍历外,还会在其原型链上面寻找可枚举属性。这样子就可以解释为啥会说 for in 遍历的性能比较差了。
那么关于这 for in 的问题话,也会引发其他一系列的问题,比如
// A.JS
Object.prototype.type = 'type';
// B.JS
const obj = { id:1, name:'name' };
const newObj = {};
// 获取obj中非id的其他属性
for(let key in obj){
if(key !== 'id'){
newObje[key] = obj[key];
}
}
console.log(newObj);
// 预期效果:{name:'name'}
// 打印结果:{ name:'name', type:'type' };
- 在扩展了 Object.prototype 后使用了 for in ,可能会出现无法预估的问题,导致后续可能存在的逻辑错误
- 所以建议不要扩展Object.prototype 或其他内置原型