揭秘 JS 继承的 “戏精家族” :原型、原型链与 new
前言
各位 前端er 朋友们,要搞懂 JS 的面向对象和继承逻辑,绕不开 原型(prototype)、隐式原型(proto)、原型链 这三个核心概念,而 new 关键字 正是串联起它们、实现实例创建的关键 “桥梁”。看到这里就已经觉得很绕了吧,没错。But!这些概念看似抽象,实则是 JS 引擎优化属性复用、实现继承的底层设计——就像家族里各有分工的成员,各司其职又紧密配合,才撑起了 JS 继承的 “大戏”。
一、函数的 “天赋”——prototype(显示原型)
咱先说说prototype,它就像函数天生带的 “天赋”,每个函数一出生就自带这个对象属性。你可以把它理解成一个 “公共仓库”,往里面放的属性和方法,所有通过这个函数创建的实例都能 “共享”。
举个例子:
Array.prototype.abc = function(){
return 'abc';
}
const arr = [];
console.log(arr.abc());
咱们给Array的prototype(数组的 “公共仓库”)加了个abc方法,然后创建一个空数组arr,它就能直接调用这个方法。这就是因为实例对象的隐式原型和构造函数的显示原型是相通的,也就是说:
实例对象的隐式原型 == 构造函数的显示原型
结果也肯定了我们的结论,输出abc:
二、对象的 “隐形翅膀”—— proto(隐式原型)
每个对象(注意是所有对象,包括函数创建的实例)都有个__proto__,它就像 “隐形翅膀”,悄悄连接着自己的 “原型长辈”。V8 引擎在找属性的时候,是个 “势利眼”—— 先找对象自己的显示属性,找不到就顺着__proto__(隐式原型)去 “原型长辈” 那里扒拉。其实也很容易理解,我们找东西肯定先找放在桌子上看得见的,再去桌子的抽屉里面找。
咱还是拿su7举例子:
Car.prototype.name = 'su7-Ultra';
function Car(color){
this.color = color;
}
const car1 = new Car('pink');
console.log(car1.name);
car1自己只有color属性,但它能通过__proto__找到Car.prototype里的name。这就是因为实例对象的__proto__ === 构造函数的 prototype,相当于car1.__proto__直接指向了Car.prototype,所以能拿到里面的name~
输出结果:
成功输出了我们的su7-Ultra!
三、“造人机器” new 关键字的骚操作
new是啥,它干了啥呢?new关键字就像个 “造人机器”,它创建实例的过程堪称 “步骤大师”,咱们拆解一下:
-
创建空对象:先造一个 “空壳子” 对象,比如
new Car()时,先弄一个{}。 -
绑定 this:把构造函数里的
this指向这个空对象,相当于告诉构造函数:“接下来给这个空壳子塞东西哈!” -
执行构造函数代码:比如
Car里的this.color = color,就是往空对象里加属性。 -
连接原型:把空对象的
__proto__直接赋值为构造函数的prototype,让实例和 “公共仓库” 打通。 - 返回对象:最后把这个 “装修好” 的对象返回出去。
上代码更清晰:
function Car(){
// const obj = {}; // 步骤1:创建空对象
this.name = 'su7'; // 步骤2,3
// obj.__proto__ = Car.prototype; // 步骤4:连接原型
// return obj; // 步骤5:返回对象
}
const car = new Car();
console.log(car.constructor); // 能找到构造函数Car,因为原型链连起来了
结果也在我们意料之中:
这么一拆解,是不是觉得new其实就是个 “流水线包工头”,把创建对象的步骤安排得明明白白~
四、原型链:JavaScript 的 “族谱”
原型都搞定了,那原型链也就是顾名思义了。原型链就是把这些__proto__和prototype串起来的 “族谱”。V8 找属性时,会沿着这个 “族谱” 往上扒,直到扒到null(族谱的 “老祖宗”,再往上没了)为止。
看这段 “祖孙三代” 的代码:
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 = 'harvest';
}
Child.prototype = new Parent(); // {lastName = 'harvest'}.__proto__ = arent.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());
觉得很长很乱吧,没关系一起来,看看 “孙子c” 怎么凭着族谱 “蹭” 祖上的东西:
1. console.log(c.card); —— 输出:10000
咱一步步看 “认祖归宗” 的过程:
- 先翻 c 自己的口袋:只有
age:18,没card,掏族谱! - 顺着
c.__proto__找爸爸的仓库(Child.prototype,也就是new Parent()的实例):爸爸的仓库里只有lastName: 'harvest',还没card,继续往上找! - 再顺着爸爸仓库的
__proto__(Parent.prototype.__proto__)找爷爷的仓库(Grand.prototype):爷爷的仓库里有card:10000(爷爷构造函数里的专属属性),找到了! - 所以直接输出爷爷给的 “启动资金” 10000—— 这就是 “戏精家族” 的传承,孙子能蹭到爷爷的银行卡💳!
2. c.house(); —— 输出:四合院
同样按族谱寻亲:
- 先翻 c 自己的口袋:没
house方法,掏族谱! - 找爸爸的仓库:只有
lastName,没有house,继续往上! - 找爷爷的仓库(
Grand.prototype):嘿,爷爷的祖传仓库里正好有house方法,直接调用! - 所以执行后输出 “四合院”—— 相当于孙子凭着族谱,直接用了爷爷的 “祖传房产” 技能!
3. console.log(c.toString()); —— 输出:[object Object]
这波是 “蹭到了家族的老老祖宗”(Object)的好处:
- 先翻 c 自己的口袋:没
toString方法,掏族谱! - 找爸爸仓库:没有,找爷爷仓库:也没有(爷爷只给了
card和house),继续往上! - 顺着爷爷仓库的
__proto__(Grand.prototype.__proto__)找 “老老祖宗”Object的仓库(Object.prototype):这里藏着 JavaScript 所有对象都能共用的toString方法! - 调用后就输出默认格式
[object Object]—— 相当于 “戏精家族” 的族谱最顶端,还连着所有对象的 “公共老祖宗”,好处能蹭到最上头!
输出结果和我们分析的一模一样:
OK,下课!
总结:“戏精家族” 的传承逻辑
-
prototype是每个 “家族长辈”(函数)的 “祖传仓库”,共享属性方法全在这; -
__proto__是每个 “家族成员”(对象)的 “隐形族谱”,负责连接上一辈的仓库; -
new是 “家族造人师”,不仅造新成员,还得给它上 “家族户口”(连族谱); - 原型链是 “完整族谱”,属性查找全靠它 “代代往上蹭”,直到蹭到
null为止。
这 “戏精家族” 的传承逻辑,本质就是 JavaScript 的继承核心 —— 不用重复造轮子,晚辈凭着族谱就能共享祖上的 “资源”,既省空间又高效。每个属性和方法的查找,都是一场有趣的 “家族寻亲记”。
开启一场“寻亲之旅”吧!