深入剖析JavaScript的原型及原型链
什么是JavaScript的原型?
原型是函数上的一个属性,它定义了构造函数制造的对象的公共祖先
原型的主要作用在于实现对象之间的属性和方法共享,从而节省内存空间,提高代码的效率
-
我们通过一段代码来接讲解,通过购买小米su7这个例子讲解
- 首先我们创建一个
function Car来作为车的构造函数
- 首先我们创建一个
function Car(color, owner) {
this.name = 'su7'
this.lang = 5000
this.height = 1400
this.color = color
this.owner = owner
}
当使用 new Car(...) 来创建新的实例时,每个实例都会具有 name、lang、height、color 和 owner 这些属性。其中 color 和 owner 的值会根据创建实例时传递的参数来确定,而 name、lang 和 height 的值则是固定的。
接下来我们就去购买小米汽车了
let tian = new Car('black', 'tiantian')
这里我们给tiantian购买了一辆su7,在构造su7的时候,车子的name,lang,height是不变的,无论是谁来购买,这三个数据是不会变的,只有主人和颜色是可以变的,但是每次购买su7的时候,还是会重新创建一份关于name,lang,height的数据,这样就造成了内存的浪费,降低了代码的效率
所以我们的代码可以改造为
Car.prototype.name = 'su7'
Car.prototype.lang = 5000
Car.prototype.height = 1400
function Car(color, owner) {
// this.name = 'su7'
// this.lang = 5000
// this.height = 1400
this.color = color
this.owner = owner
}
let tian = new Car('black', 'tiantian')
console.log(tian);
console.log(tian.name); //隐式具有,也就是继承了
代码通过在 Car 的原型对象 Car.prototype 上定义了 name、lang 和 height 属性。
当创建 tian 这个 Car 的实例时,虽然在构造函数内部没有直接为实例设置 name、lang 和 height 属性,实例仍然可以访问到在原型对象上定义的这些属性。
所以当打印 tian 时,会显示出包含从原型继承的属性以及在构造函数中设置的 color 和 owner 属性的对象。而打印 tian.name 时,能够获取到在原型上定义的 'su7' 。

这样就实现了name,lang,height的数据共享,节约了内存
这里我们已经对原型有一个初步的认识了,接下来我们继续讲解什么是prototype属性
Car.prototype.product = "xiaomi"
function Car() {
this.name = "su7"
}
var car = new Car();
console.log(car);
console.log(car.name);
console.log(car.product);
这段代码的执行结果为

现在如果我们想修改product的值,可以直接修改吗?
car.product = "iphone"
现在添加这段代码,我们可以看到的运行效果为

很显然并不能通过这种方式去修改prototype里面的product
这里我们就明白了一个重要的结论:
- 实例对象可以修改显示继承到的属性
- 实例对象无法修改隐式继承到的属性(原型上的属性)
- 实例对象无法给原型新增属性
- 实例对象无法删除原型上的属性
对象里面到底有什么?
我们将这段代码放在浏览器运行
<script>
function A() {}
let a = new A();
console.log(a);
</script>
你可以看到这样的结果

可以看到在A{}里面有[[Prototype]],[[Prototype]]为对象的原型
注:在谷歌浏览器显示为[[Prototype]],别的浏览器可能显示为__proto__
我们继续修改代码
<script>
function A() {}
let a = new A();
console.log(A.prototype);
console.log(a.__proto__);
</script>
这段代码的结果为

结果显而易见,对象的原型 === 它的构造函数的原型
在对象的原型又叫隐式原型,函数的原型又叫显示原型,所以结论可以改为
对象的隐式原型 === 创建它的构造函数的显式原型
这里可以理解为对象用自己来继承函数里的属性,对象用它的原型承接函数的原型的熟悉
对象可以访问到构造函数的显式原型属性,为什么?
回到最初的购买su7
Car.prototype.product = "xiaomi"
function Car() {
this.name = "su7"
}
var car = new Car();
console.log(car);
console.log(car.name);
console.log(car.product);
现在我们来讨论为什么console.log(car.product);可以打印出结果?
其实V8引擎在执行这段代码时,首先会去car的显示属性里面寻找,然后再去隐式原型里面找,然后隐式原型又相当于其构造函数的显示原型,这便是原型链
原型链
通过以下代码,我们来讲解一下原型链
<script>
function A() {}
let a = new A();
console.log(a);
</script>
我们通过不断的展开对象的__proto__,我们就能够发现一个事情

从这里我们可以看出来两个信息:
a.__proto__ === A.prototypeA.prototype.__proto__ === Object.prototype
为了方便理解,我们作图讲解

在 JavaScript 中,Object.prototype.__proto__ 的值为 null , Object.prototype 处于原型链的顶端,没有更高层次的原型
除了 Object 构造函数,几乎所有构造函数的原型对象最终都会通过原型链追溯到 Object.prototype,以继承一些通用的方法和属性,比如 toString 、 valueOf 等
我们通过一个小demo更加直观的感受一下原型链
function GrandFather() {
this.age = 60;
this.like = 'drink';
}
Father.prototype = new GrandFather();
function Father() {
this.age = 40;
this.fortune = 1000;
}
Son.prototype = new Father();
function Son() {
this.age = 20;
}
let son = new Son();
console.log(son.age);
console.log(son.fortune);
console.log(son.like);
首先定义了 GrandFather 构造函数,并设置了属性 age 和 like
然后将 Father 的原型对象设置为 GrandFather 的一个实例,接着定义了 Father 构造函数,设置了属性 age 和 fortune
之后将 Son 的原型对象设置为 Father 的一个实例,再定义了 Son 构造函数,设置了属性 age
我们可以看到结果为:

console.log(son.age) 输出 20 ,因为实例自身的 age 属性会覆盖原型链上的同名属性。
console.log(son.fortune) 输出 1000 ,这是从原型 Father 继承的
console.log(son.like) 输出 drink ,这也是从原型链上的 GrandFather 继承的
这里我们可以得出一个结论:
js引擎在查找属性时,会沿着原型链查找。即对象的隐式原型向上查找,找不到就沿着隐式原型的隐式原型继续查找,直到找到Object.prototype._proto__,如果没找到就返回undefined。null为止
网易面试题:世界上所有的对象都有隐式原型吗?
答案是否定的
有一种写法是没有隐式原型的
首先我们来看这段代码
let a = {
name:'tom'
}
let obj = Object.create(a)
console.log(obj)
这段代码的结果为

这里我们可以看出来这个方法的作用为创建一个新对象,让新对象隐式的继承a的属性
那如果我们使用let obj = Object.create(null)呢?
直接公布结果:

这样创建出来的对象是没有隐式原型的
总结
本文深度的解析了原型及原型链,从各个角度出发结合代码去分析过程原理以及结果,并结合面试题深入剖析知识点
相信看到这里的你一定会有所收获的!!!!
转载自:https://juejin.cn/post/7384636970034937906