网易面试题:所有对象身上都有隐式原型吗?为什么?
这道题你会吗?原型和原型链作为js语言的核心概念之一,为我们提供了强大的对象继承机制。今天我们将详细讲解js的原型以及原型链,帮助新手从零开始理解这一重要概念,学完这些,你就能完美解答网易的这道面试题啦!
原型 (显式原型)
- 定义:原型(prototype)是函数天生具有的属性,它定义了构造函数制造出来的对象的公共祖先。通过该构造函数创建的对象,可以隐式的继承函数原型上的属性和方法。
我们来看代码示例:
javascript
function Person() {
this.name = '小明'
this.age = 18
}
Person.eat = function() {
console.log('想吃面条')
}
let p = new Person()
let p2 = new Person()
p.eat()//报错
我们定义了一个构造函数Person,并创建了两个它的实例对象p和p2,Person里面有属性name和age,我们还往Person上添加了一个方法eat。
如果我们输出p,可以得到:
p这个实例对象里会有name和age,但是没有eat,如果调用p.eat会直接报错,eat也是Person里的方法,为什么p没有呢?我们看name和eat的区别:name前面有this.
。p是通过new创建出来的对象,它会继承Person中的this.name
和this.age
。关于它是如何继承的如果你还不了解请看《关于数据类型分类,构造函数和包装类的重要底层原理》这篇文章中的new的过程这一部分。而eat是只在Person里的。
但是如果我们在Person的原型上添加一个say方法(原型prototype是函数天生就具有的属性):
javascript
Person.prototype.say = function() {
console.log('hello,world!')
}
console.log(p);
p.say()
我们会看见结果是:
输出p里面仍然只有name和age,但我们调用p.say却是可行的,这是因为say是在Person的原型上,p可以隐式地继承在Person.prototype上的属性和方法。
ini
console.log(p.say===p2.say)
p和p2是两个独立的对象,如果我们输出这个,得到的结果会是true
,这说明say方法就是Person原型上的那个,所以说原型定义了构造函数制造出来的对象的公共祖先。
-
意义:可以提取共有属性,简化代码的执行
inifunction Car(owner,color) { this.name = 'BMW' this.lang = 4900 this.height = 1400 this.owner = owner this.color = color } let car = new Car('小A','red') let car2 = new Car('小B','pink')
我们定义一个构造函数Car,里面有name,lang和height属性,还有需要传参数进来的owner和color,创建两个实例对象car和car2,那么两个实例对象中都会有这些属性,且name,lang和height的值是固定的。如果我们用prototype将其提取:
iniCar.prototype.name = 'BMW' Car.prototype.lang = 4900 Car.prototype.height = 1400 function Car(owner,color) { this.owner = owner this.color = color } let car = new Car('小A','red') let car2 = new Car('小B','pink')
那么我们就能简化代码,而实例对象仍能正常访问到这几个属性。
-
原型上的属性修改只能原型自己操作,实例对象无权修改
arduinocar.name = '奔驰' console.log(car.name);//奔驰 console.log(car); console.log(car2.name);//BWM console.log(car2);
如果我们将car.name修改为奔驰
,来看一下它们的输出:
实际上car.name = '奔驰'
只是往对象car里面新增加了一个值为奔驰的name属性,并没有修改原型上的name,从car2的输出我们就可以看出来,原型上的name属性是并未被修改的。但若是原型自己修改的属性:Car.prototype.name = '奔驰'
,则被Car创建的所有实例对象去访问name都会是奔驰
。
对象原型 (隐式原型)
在JavaScript中,每个实例对象都有一个特殊的属性称为"隐式原型"(__proto__
或[[prototype]]
),它指向该对象的构造函数的原型。即实例对象的隐式原型===构造函数的显式原型。
继续以之前的代码为例:
ini
console.log(car.__proto__===Car.prototype);
console.log(car.__proto__);
输出的结果是:
new的过程
我们之前在《关于数据类型分类,构造函数和包装类的重要底层原理》这篇文章中提到过new的过程有三个步骤,但其实是不完整的,第三个步骤应该是将 this 的隐式原型指向构造函数的显式原型
,之后再进行this对象的返回。
- 创建this空对象
- 执行构造函数中的逻辑,将属性和方法添加到 this 对象上
- 将 this 的隐式原型指向构造函数的显式原型
- 返回this对象
这样子创建的对象才会能够访问构造函数的显式原型。
原型链
构造函数的原型也是一个对象,而对象就会有隐式原型,所以构造函数的原型也有隐式原型:
javascript
function Foo() {}
let foo = new Foo();
foo.__proto__ === Foo.prototype
Foo.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null
foo是构造函数Foo的实例对象,所以foo的隐式原型foo.__proto__
恒等于Foo的原型Foo.prototype
,而Foo原型的隐式原型则指向对象的构造函数原型Foo.prototype.__proto__ === Object.prototype
,对象的构造函数原型的隐式原型则指向null,再往上就没有了。
顺着对象的隐式原型不断向上查找上一级的隐式原型,直到找到目标或者null为止,这种查找关系,就叫做原型链。
网易面试题:所有对象身上都有隐式原型?
看到这里,你的答案是肯定的吗:所有对象身上都有隐式原型。不不不,答案没有那么简单,不得不说,这道题是真的有点坑。
绝大多数的对象身上都有隐式原型,而那个没有的特例,让我们一起来看看吧:
javascript
let c = Object.creat(null)
如果我们用Object.creat(null)
(不能为空,一定要有参数)方法来创建一个空对象,那么这个对象是会没有隐式原型的,只有这一个特例。
来挑战一下这张图,如果你能将每条链都看懂,那么你就彻底掌握原型与原型链啦:
你成功了吗ヾ(◍°∇°◍)ノ゙