深入剖析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.prototype
A.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