前言
在 JavaScript 里,"原型"这个词听起来高大上,其实就是一个"默认备胎":当对象自己找不到属性时,就沿着原型这条暗道去"亲戚家"借。没有类、没有蓝图,仅靠这条备胎链,就能把公共方法层层复用,让内存省一半、代码少一半。本文只聊"原型"本身------prototype、__proto__ 这些眼前能用的工具,把"借东西"的流程画成一张家谱图,帮你先看清"亲戚"是谁、住哪、怎么串门。至于后面更高阶的封装、多态、模块化,等我们把这条链走熟再升级也不迟。
一: 原型 prototype
又称显示原型,函数天生拥有的一个属性 ,将构造函数中的一些固定的属性和方法挂载到原型上,在创建实例的时候,就不需要重复执行这些属性和方法了,我们先来创造一个环境,主角依然是我们的小米 su7 ,su7 的属性有无数个,但是各个车主只需要选择并改动的属性并没有那么多,这个时候我们就能用得到原型。
ini
Car.prototype.name = 'su7-Ultra'
Car.prototype.lang = 4800
Car.prototype.height = 1400
Car.prototype.weight = 1.5
function Car(color) {
this.color = color
}
const car1 = new Car('pink')
const car2 = new Car('green')
console.log(car1);
用原型之后我们只需要输入想要的颜色即可,不需要反反复复创建函数。同时挂载在原型上的属性是可以直接被实例对象访问到的(如下图)

并且实例对象无法修改 构造函数 原型上的属性值,
ini
Person.prototype.say = '我太帅了'
function Person() {
this.name = '饶总'
}
const p = new Person()
p.say = 'hello'
const p2 = new Person()
console.log(p2.say);
这个时候同时有两个 key 都为 say ,但 value 不相同,一个被挂在构造函数的原型上,一个被挂在第一个实例对象 p 上,按照上面说法实例对象无法修改构造函数原型上的属性值,但是打印出来真是这样吗,究竟是 '我太帅了' ,还是 'hello',我们来揭晓答案

果真是实例对象无法修改构造函数原型上的属性值。
二:对象原型 __proto__
又称隐式原型,每一个对象都拥有一个 __proto__ 属性,该属性值也是一个对象, v8在访问对象中的一个属性时,会先访问该对象中的显示属性,如果找不到,就回去对象的隐式原型中查找,实例对象的隐式原型 === 构造函数的显示原型,所以如果实例对象的隐式原型找不到那么就再会去构造函数的显示原型上找
这不得不再引出一个概念------ 原型链 :v8 在访问对象中的属性时,会先访问该对象中的显示属性,如果找不到,就去对象的隐式原型上找,如果还找不到,就去__proto__.__proto__ 上找,层层往上,直到找到null为止。这种查找关系被称为原型链
为了更好的理解它,我们来举个继承例子
javascript
function Parent() {
this.lastName = '张'
}
Child.prototype = new Parent() // {lastName: '张'}.__proto__ = Parent.prototype
function Child() {
this.age = 18
}
const c = new Child()
console.log(c.lastName);
在实例对象中我们只能找到儿子的年龄属性,姓氏张是儿子从父亲那里继承的,我们要查到儿子的姓氏,根据原型链原理我们先从实例对象 c 中找有没有显示属性是关于姓氏的,很明显并没有,接着就去实例对象的隐式原型上找,也没有,最后就来到了构造函数的显示原型上查找,在代码的第四行可以看到构造函数的显示原型被赋值上了 lastName 属性,所以最终是否可以查找得到姓氏张呢?我们来直接看结果 
好你说这个也太简单了吧,就父子继承而已。话不多说我再附上一串代码和打印结果
javascript
Grand.prototype.house = function() {
console.log('四合院');
}
function Grand() {
this.card = 10000
}
Parent.prototype = new Grand() // {card: 10000}.__proto__ = Grand.prototype.__proto__ = Object.prototype.__proto__ = null
function Parent() {
this.lastName = '张'
}
Child.prototype = new Parent() // {lastName: '张'}.__proto__ = Parent.prototype
function Child() {
this.age = 18
}
const c = new Child() // {age: 18}.__proto__ = Child.prototype
console.log(c.card);
c.house()
// console.log(c.toString());

这里我们要注意一点:如果让你查找一个整个页面都没有的属性又该会是什么打印结果呢?我们注意看上面最后一行注释掉的代码,他的输出结果如下

他是直接找到了全局的对象上,经历了一遍原型链查找在 Object.prototype上找到,如果再不找到最终就会停留在null上 ,下面放一张 js 界中广为流传的一张图,如果你能看懂那么你就是彻底会了!

三:new 在干什么?
这时候你会说什么?上篇文章不是讲了 new 究竟干了些什么吗,怎么又问,不必惊讶,其实上次没讲全,这次来带你真正看看 new 究竟究竟都干了些什么(这绝对是最终理解)直接一套小连招先上五个步骤
- 创建一个空对象
- 让构造函数中的
this指向这个空对象 - 执行构造函数中的代码 (等同于往空对象中添加属性值)
- 将这个空对象的隐式原型(
__proto__) 赋值成 构造函数的显示原型(prototype) - 返回该对象
再上代码(加注释)
javascript
Car.prototype.run = function() {
console.log('running');
}
function Car() { // new Function()
// const obj = {} //1
// Car.call(obj) // call 方法将 Car 函数中的 this = obj 2
this.name = 'su7' // 3
// obj.__proto__ = Car.prototype // 4
// return obj 5
}
const car = new Car() // {name: 'su7'}.__proto__ == Car.prototype
car.run()
最后输出

结语
- 显式原型(
prototype)是函数自带的"样板房",所有实例都能来蹭住。 - 隐式原型(
__proto__)是实例手里的"门禁卡",刷卡就能进样板房找方法。 - 原型链就是一张"门禁卡链":刷不到就再刷上一层的卡,直到
null到头。 new的五步曲:空对象→认证→绑卡→执行→返回,一口气把"样板房"继承给新实例。
把这四点串成一张地铁图,以后看任何"找不到属性"的问题,先问一句:它刷卡刷到第几站了?原型链通了,继承就不再是黑魔法。