普通视图

发现新文章,点击刷新页面。
今天 — 2025年11月20日首页

揭秘 JS 继承的 “戏精家族” :原型、原型链与 new

2025年11月20日 11:00

前言

各位 前端er 朋友们,要搞懂 JS 的面向对象和继承逻辑,绕不开 原型(prototype)、隐式原型(proto)、原型链 这三个核心概念,而 new 关键字 正是串联起它们、实现实例创建的关键 “桥梁”。看到这里就已经觉得很绕了吧,没错。But!这些概念看似抽象,实则是 JS 引擎优化属性复用、实现继承的底层设计——就像家族里各有分工的成员,各司其职又紧密配合,才撑起了 JS 继承的 “大戏”。

一、函数的 “天赋”——prototype(显示原型)

咱先说说prototype,它就像函数天生带的 “天赋”,每个函数一出生就自带这个对象属性。你可以把它理解成一个 “公共仓库”,往里面放的属性和方法,所有通过这个函数创建的实例都能 “共享”

举个例子:

Array.prototype.abc = function(){
    return 'abc';
}
const arr = [];
console.log(arr.abc());

咱们给Arrayprototype(数组的 “公共仓库”)加了个abc方法,然后创建一个空数组arr,它就能直接调用这个方法。这就是因为实例对象的隐式原型和构造函数的显示原型是相通的,也就是说:

实例对象的隐式原型 == 构造函数的显示原型

结果也肯定了我们的结论,输出abc:

image.png

二、对象的 “隐形翅膀”—— 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~

输出结果:

image.png

成功输出了我们的su7-Ultra

三、“造人机器” new 关键字的骚操作

new是啥,它干了啥呢?new关键字就像个 “造人机器”,它创建实例的过程堪称 “步骤大师”,咱们拆解一下:

  1. 创建空对象:先造一个 “空壳子” 对象,比如new Car()时,先弄一个{}
  2. 绑定 this:把构造函数里的this指向这个空对象,相当于告诉构造函数:“接下来给这个空壳子塞东西哈!”
  3. 执行构造函数代码:比如Car里的this.color = color,就是往空对象里加属性。
  4. 连接原型:把空对象的__proto__直接赋值为构造函数的prototype,让实例和 “公共仓库” 打通。
  5. 返回对象:最后把这个 “装修好” 的对象返回出去。

上代码更清晰:

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,因为原型链连起来了

结果也在我们意料之中:

image.png

这么一拆解,是不是觉得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方法,掏族谱!
  • 找爸爸仓库:没有,找爷爷仓库:也没有(爷爷只给了cardhouse),继续往上!
  • 顺着爷爷仓库的__proto__Grand.prototype.__proto__)找 “老老祖宗”Object的仓库(Object.prototype):这里藏着 JavaScript 所有对象都能共用的toString方法!
  • 调用后就输出默认格式[object Object]—— 相当于 “戏精家族” 的族谱最顶端,还连着所有对象的 “公共老祖宗”,好处能蹭到最上头!

输出结果和我们分析的一模一样:

image.png

OK,下课!

总结:“戏精家族” 的传承逻辑

  • prototype是每个 “家族长辈”(函数)的 “祖传仓库”,共享属性方法全在这;
  • __proto__是每个 “家族成员”(对象)的 “隐形族谱”,负责连接上一辈的仓库;
  • new是 “家族造人师”,不仅造新成员,还得给它上 “家族户口”(连族谱);
  • 原型链是 “完整族谱”,属性查找全靠它 “代代往上蹭”,直到蹭到null为止。

“戏精家族” 的传承逻辑,本质就是 JavaScript 的继承核心 —— 不用重复造轮子,晚辈凭着族谱就能共享祖上的 “资源”,既省空间又高效。每个属性和方法的查找,都是一场有趣的 “家族寻亲记”。

开启一场“寻亲之旅”吧!

昨天 — 2025年11月19日首页

JS 对象:从 “散装” 到 “精装” 的晋级之路

2025年11月19日 10:48

前言

在阅读前先解决一个问题,那就是啥是对象?说白了,JS 里的 “对象” 就是个 “万能收纳箱”—— 不管是数字、字符串这些零散数据,还是能干活的方法,都能往里面塞,把杂乱的信息规整得明明白白。而支撑这个 “收纳箱体系” 的,一边是像 “孤家寡人” 似的原始类型(string、number、boolean、undefined、null、Symbol、bigInt),它们单打独斗,不能挂额外属性;另一边是热热闹闹的引用类型(也就是对象家族),它们是 “群居选手”,能装能存还能扩展。

今天咱就来扒一扒这个 “收纳箱” 是咋造出来的、背后有啥隐藏操作。

一、对象的 “诞生记”:三种创建方法

想要拥有一个对象,JS 给了你三种 召唤术

1. 对象字面量:“我懒我骄傲”

就像我们点外卖直接选套餐,对象字面量是最直接的创建方式:

const obj = {
    a: 1
};

你肯定会说:什么?就这一点代码?对,就这一点,对象就到手了!

2. new Object ():“走流程,咱正规”

如果你是个 “流程控”,可以用构造函数正儿八经创建:

const obj = new Object(); // 构造函数
obj.name = 'henry';
const abc = 'age';
obj[abc] = 18;   // 删除

delete obj[abc];
console.log(obj);

打印结果:

image.png

这种方法也可行,非常正规,赞!

3. new 自定义构造函数:“批量生产,效率王者”

当你需要 “复制粘贴” 式创建一堆相似对象时,构造函数就是你的 “生产流水线”!比如我们造个Person构造函数:

function Person() {

}
const obj = new Person();
console.log(obj);

image.png

二、构造函数:“批量造对象的黑作坊”

构造函数就像个 “对象工厂”,被 new 调用时才算是真正的构造函数。当你需要批量创建对象时,它就是你的 “效率神器”!

其中总共有2种方法,我称之为手工小作坊自动化工厂

你们最喜欢的代码环节:

// 手工小作坊:每次造对象都得重复写
function Insert(name, age, job) {
    const obj = {
        name: name,
        age: age,
        job: job
    }
    return obj;
}
console.log(Insert('henry',19,'coder'));

结果也正确的打印出了我们赋的值:

image.png

但是这种写法就有一个缺点!我只是给了一组数据输出结果,那如果是10个呢,100个呢?甚至是老板让你把全公司人的信息输入进去呢?难道你一个一个去console.log?显然在庞大的数据下这种肯定是不推荐的,那就有请另外一位--自动化工厂闪亮登场!

// 自动化工厂:new一下,对象自动造好
function Car(color) {
    this.name = 'su7';
    this.height = '1400';
    this.long = '4800';
    this.weight = '1500';
    this.color = color;
}
const car1 = new Car('purple'); // 实例化一个对象
console.log(car1);

image.png

拿代码中的小米su7举例子:

如果雷总想让你批量生产su7,你如果用手工小作坊 的方法,那得多少行代码啊!每辆车的型号信息都得重复输入,效率低的没眼看。那么怎么去解决呢?简单!你很快就发现,每辆车它的长度啊,高度啊,重量啊等等都是一样的,没必要去重复输入,只有颜色不一样而已,所以为了提高效率,我们有请this带来它的自动化工厂

function Car(color) {
    this.name = 'su7';
    this.height = '1400';
    this.long = '4800';
    this.weight = '1500';
    this.color = color;
}
const car1 = new Car('purple'); 
console.log(car1);
const car2 = new Car('blue');
console.log(car2);

image.png

如此这般,我们只需传输每辆车不同的数据(比如颜色),其他的已经固定好的数据我们就可以省略不写,大大提高了我们敲代码的效率,学废了没?

三、new 关键字:“构造函数的幕后BOSS”

你以为 new 只是个关键字?它可是 “对象诞生” 的幕后导演!它干了三件大事:

  1. 创建一个 this 对象:相当于搭了个空舞台;
  2. 执行构造函数代码:往 this 上狂加属性方法,把舞台布置好;
  3. 返回 this 对象:把布置好的舞台交给你。

咱们用代码模拟一下 new 的逻辑,一目了然:

function person() {
    const _this = {};   // 第一步:造个空this
    _this.name = 'su7'; // 第二步:往this上加东西
    _this.color = 'green';
    return _this;       // 第三步:返回this
    const obj = person();
    console.log(obj);   // 看看实力
}

看看导演的成果:

image.png

this已经被官方征用了,所以我们这里只是用_this模拟this的对象,大家不要搞混了哈!

四、包装类:“原始类型的隐藏马甲”

依旧先上代码:

var str = 'hello'; 
console.log(str.length);

image.png

打印输出的结果为5,为hello的长度,看似没有问题。But!有人会问:作者你之前不是说原始类型不是不能有属性方法吗?那str.length是咋来的?随着质疑声越来越大,我也不含糊,这就不得不讲讲包装类了。

JS 是个 “戏精”,当你用原始类型字面量时,它会偷偷给你套个 “包装对象” 的马甲(比如new String()),让原始类型也能临时 “装” 成对象用用。但这马甲很 “薄”——参与运算时会自动拆成原始类型,赋值时还会把临时加的属性偷偷删掉

具体来说就是:因为 js 是弱类型语言,所以只有在赋值语句执行时才会判断值的类型(typeof),当值被判定为原始类型时,就会自动将包装对象上添加的属性移除

拿刚刚的hello举例子:

var str = 'hello'; // 看似是原始类型,实际背后是new String('hello')
str.length = 2;    // 试图给它加个属性
console.log(str.length);  // 猜猜是2吗?不,还是5!因为马甲被拆了,临时属性没了
console.log(typeof(str)); // 类型还是string,马甲只是临时穿穿

结果和预期一样:

image.png

总结

其实 JS 里的对象逻辑,本质就是 “把零散的数据和功能打包成整体”—— 字面量是 “随手包”,构造函数是 “批量生产线”,new 是 “生产线的核心开关”,而包装类则是 “让零散原始值临时享受对象待遇的便民服务”。

这些设计看似花里胡哨,实则都是为了平衡 “易用性” 和 “专业性”:既不让新手觉得创建对象太复杂,也能让老手通过构造函数、包装类的底层逻辑玩出花样。

通过这篇文章,下次再写对象相关代码时,你不妨想想:我这是在 “随手包个小物件”,还是在 “开生产线批量造货”?原始值的 “马甲” 会不会在运算时掉下来?想通这些,你就不再是 “只会用对象” 的新手,而是 “懂对象底层逻辑” 的进阶玩家 —— 毕竟 JS 的魅力,就在于这些看似 “反常识” 却又 “超合理” 的设计里面!

❌
❌