把原型链画成地铁图:坐 3 站路就能看懂 JS 的“继承”怎么跑

前言

在 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 究竟究竟都干了些什么(这绝对是最终理解)直接一套小连招先上五个步骤

  1. 创建一个空对象
  2. 让构造函数中的 this 指向这个空对象
  3. 执行构造函数中的代码 (等同于往空对象中添加属性值)
  4. 将这个空对象的隐式原型(__proto__) 赋值成 构造函数的显示原型(prototype)
  5. 返回该对象

再上代码(加注释)

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()

最后输出

结语

  1. 显式原型(prototype)是函数自带的"样板房",所有实例都能来蹭住。
  2. 隐式原型(__proto__)是实例手里的"门禁卡",刷卡就能进样板房找方法。
  3. 原型链就是一张"门禁卡链":刷不到就再刷上一层的卡,直到 null 到头。
  4. new 的五步曲:空对象→认证→绑卡→执行→返回,一口气把"样板房"继承给新实例。

把这四点串成一张地铁图,以后看任何"找不到属性"的问题,先问一句:它刷卡刷到第几站了?原型链通了,继承就不再是黑魔法。

相关推荐
bank_dreamer1 小时前
VSCODE前端代码风格格式化
前端·css·vscode·html·js·prettier·代码格式化
人工智能训练1 小时前
前端框架选型破局指南:Vue、React、Next.js 从差异到落地全解析
运维·javascript·人工智能·前端框架·vue·react·next.js
IT_陈寒1 小时前
90%的Python开发者不知道:这5个内置函数让你的代码效率提升300%
前端·人工智能·后端
网络点点滴1 小时前
Vue3的生命周期
前端·javascript·vue.js
梵得儿SHI2 小时前
Vue 核心语法之组件基础与通信:从创建到注册的完整指南
前端·javascript·vue.js·组件化开发·全局注册·vue组件的本质·局部注册和异步组件
MQliferecord2 小时前
如何快速实现响应式多屏幕适配
前端
欧阳的棉花糖2 小时前
不用记复杂路径!3 步让你的 JS 脚本像 “vue create” 一样好用
javascript
韭菜炒大葱2 小时前
从回调到async/await:JavaScript异步编程的进化之路
前端·javascript·面试