通常创建一个对象实例有2种方法。比如我们要创建一个乐乐特工对象。
第一种: 通过构造函数创建。
构造函数是一个用于创建和初始化对象的特殊方法。在JavaScript中,构造函数通常以大写字母开头。
第二种:通过类创建对象实例。
ES6引入了class语法,使得创建对象和继承更加直观。
在上述代码中,Agent2是一个类,通过new Agent2(‘乐乐’, 12) 创建了一个新的Agent特工对象。
【考一考】: 两种实例化出来的实例对象是同一个对象吗?
【正确解答】:
不相等,两种方法实例出来的对象绝对不是同一个。
即使它们内部的属性和值完全一样,它们也是两个独立的、不同的对象。在JavaScript中,对象是比较引用(即在内存中的地址),而不是比较值。
我们来验证一下:
为什么是false 呢?因为new 每次执行时,都会在内存中开辟一块全新的空间来存放数据。agentNo1 住在内存A 的房子里,agentNo2住在内存B 的房子里。虽然它们房子里的家具(属性和值)一模一样,但是门牌号码(内存地址)是不同的,所以他们不是同一个人。
有一个很重要的细节补充:
如果你用同一种构造方式实例化出来两个对象,他们也绝对不相等。
原来,只要加了new,就是创建了新生命,哪怕基因(代码参数)一样,也是两个独立的个体。
【考考你】:那你知道什么情况下,两个实例对象会相等呢?
1 、class Agent2 是一个构造模板:使用 class 语法定义了 Agent2,它的 constructor 会在实例化时接收 name 和 age 参数,并将其挂载到实例对象上。
2 、new Agent2 创建独立对象:执行 const agentNo2 = new Agent2('乐乐', 12) 时,JavaScript引擎会在内存中开辟一块新空间,创建一个全新的对象实例。agentNo2 变量存储的是指向这块内存空间的引用地址(指针) ,而不是对象本身。
3 、对象赋值是传递引用:执行 const agentNo4 = agentNo2 时,并没有在内存中复制出一个新对象,而是将 agentNo2 保存的内存地址赋值给了 agentNo4。此时,agentNo4 和 agentNo2 指向了内存中的同一个对象。
4 、严格等于 === 的比较逻辑:对于对象类型,=== 比较的不是对象的属性值是否相同,而是比较它们的内存地址是否一致。因为 agentNo4 和 agentNo2 指向同一块内存地址,所以 agentNo4 === agentNo2 的结果为 true。
【考考你】:一个构造函数生成2个实例对象,唯一可以共享的是什么呢?
【正确答案】:原型(Prototype)
虽然实例本身的私有地盘(name,age)绝不共享,但是他们会共享一个公共的对象,这个对象叫做原型。
【代码解释】:
1、function Agent1 是构造器(私有地盘)
Ø 每次new Agent1 时,内部通过this.name = name **** 和 this.age = age ****给实例分配私有属性。
Ø agentNo2 和 agengNo3 各自占有独立的内存空间,存着自己的“乐乐1号”和“乐乐2号”。
2、 Agent1.prototype.sayHi 是一个公共方法(共享地盘)
Ø 这里显式地把sayHi 函数挂载到了 Agent1.prototype 这个原型对象上。
Ø 它只占一份内存,不管是agentNo2 还是agentNo3 ,他们自身没有sayHi的代码副本。
3、 调用时的隐式查找(agentNo2.sayHi())
Ø 当引擎执行agentNo2.sayHi()时,查找顺序是:
1) 先找agentNo2自身有末有sayHI -> 没有。
2) 顺着隐式原型agentNo2.__proto__找到Agent1.prototype -> 找到了!
Ø 在执行时,sayHi内部的this会动态绑定到当前的调用者。所以agentNo2.sayHi() 里的this是agentNo2,打印出“乐乐1号”;agentNo3.sayHi() 里的this是agentNo3,打印出“乐乐2号”。
【问】:顺着隐式原型agentNo2.__proto__找到Agent1.prototype -> 找到了!到底是啥意思?
【答】:你这句话抓住了JavaScript原型链中的最核心的链接关系。这背后隐藏着3个非常巧妙的机制。
1、 谁搭建了这座桥?(new 操作符的暗中操作)。
当你执行 const agentNo2 = new Agent1(…)时,new 关键字不仅帮你执行了Agent1函数并返回了对象,它还暗中做了一件极其重要的事: 它把实例的__proto__属性,强行指向了构造函数的prototype属性。
这就相当于new 在底层默默执行了这样一行代码:
agentNo2.proto = Agent1.prototype;
这就是为什么你能“顺着agentNo2.__proto__找到 Agent1.prototype“ 的根本原因——这座桥梁是new给你铺好的。
2、 为什么要顺着找?
当引擎看到agentNo2.sayHi() 时,它的查找规则是绝对自私的,先看自己有没有:
1) 第一步:查agentNo2 自身(私有地盘)。有没有sayHi?没有。
2) 第二步:自己没有,就顺着桥走。通过 agentNo2.__proto__走到公共地盘 Agent.prototype。有没有 sayHi?有!找到了,执行。
3、 这条链路可以用代码严格证明。
💡 ****总结成一个黄金法则: 无论你用的是 function 还是 class,只要是通过 new 创建出来的对象,都永远遵循这个等式: 实例.proto === 构造函数.prototype
这就是JavaScript面向对象体系最底层的“任督二脉”,打通了这一点,原型链对你来说就再也没有秘密了!
【考考你】: 你肯定会说,没有啊为啥class类的没有看见用到prototype 啊。
【解答】:
眼睛看不到的不一定就是事实哦。class 类是有prototype 的!
你没看见它,并不是因为它不存在,而是因为class 语法把它“隐藏起来了”,自动帮你处理了挂载逻辑。
我们用事实来说话。针对你代码中的Agent2,你可以自己在控制台里敲下面一行代码测试一下。
这就证明了:class 底层依然完全依赖prototype机制,且实例的隐式原型依然指向类的显式原型。
1. JavaScript 引擎在解析这段代码时,在幕后自动帮你做了这件事:它识别出 sayHi 是一个方法,然后自动把它挂载到了 Agent2.prototype 对象上。你不需要写 Agent2.prototype.sayHi = ...,引擎替你干了。
总结
- 真相:class 本质上是构造函数的语法糖,它绝对有 prototype,实例也绝对是通过 proto 去找共享方法的。
- 设计初衷:class 的设计目的,就是为了把繁琐、容易出错的 prototype 操作隐藏起来,让开发者用更直观、更像其他面向对象语言(如 Java/C++)的写法来定义类。你可以理解为:class 是 prototype 的一件漂亮外套,穿了外套不代表身体没了,只是你看不见了而已。
【考考你】: proto 是谁的?prototype 又是谁的?
【回答】:
这是一个极其核心的问题!搞懂了这两个属性“归谁所有”,原型链的迷雾就彻底散开了。
用最简明的话来回答:
- prototype 是 构造函数(类)的专属财产。
- proto 是 实例对象 的专属财产。
一个生动的比喻帮你彻底记住:
想象构造函数是一家建筑开发商(Agent2),它建了一个公共小区公园(Agent2.prototype),公园里有秋千、滑梯(sayHi 等共享方法)。
想象实例对象是买这开发商房子的业主(agentNo2)。业主自己家有私人卧室(name, age 私有属性),但自己家没秋千。
开发商在卖房时,给每位业主发了一张小区门禁卡(agentNo2.proto)。
- prototype(小区公园)是属于开发商的。
- proto(门禁卡)是属于业主的。
- 门禁卡指明了你可以去哪个公园玩:业主的门禁卡 === 开发商建的公园。
当业主想玩秋千(调用 agentNo2.sayHi())时,自己家没有,就拿着门禁卡(proto)去卡上指明的公园(prototype)里玩!
【考考你】:可以造两个一模一样的构造函数吗?
【答案】:
这个问题触及了JavaScript语言的底层逻辑。
简单回答:绝对不行,如果有2个一模一样的构造函数,后面的会直接把前面的“撕毁覆盖”。
让我们用大脑总部最严密的监控录像,把这个覆盖的物理动作给你还原:
【考考你】:第二个Agent中没有传入age啊,为啥不是应该输入undefined 或者输出乐乐?
【解惑】:this.age = 12; 这个是在Agent中写死的,所以不需要传。
【考考你】:在构造函数中的this.name 可以在函数外面使用吗?因为在Vue中我们似乎到处都可以使用this,对不对?
我直接给你一个明确的答案:this.name绝对不能在方法(函数)外面单独使用!它是个“变色龙”,只有被函数包裹起来,并且被调用时,它才有意义。
为什么你在Vue里觉得this.name随处可用,而这里看不懂了呢?因为Vue在背后偷偷帮你做了一件大事!
让我们用大脑总部的剧本,把这2者的区别彻底拆开:
1、 原生JS里的this:工位上的“本人”
在原生JavaScript里,this就像是一个代词,相当于中文里的“本人”或者“我自己”。
你仔细看我们写的sayHi方法:
当乐乐1号angentNo2.sayHi(),乐乐1号站到了共享台前拿起对讲机讲话。此刻乐乐1号是本人,所以this指向乐乐1号,this.name 读到 的是乐乐1号私人表上的“乐乐1号”。
如果在函数外面写this.name会怎么样呢? 比如在代码里敲出如下一行:
这就好比在大脑总部的走廊里(全局作用域),突然有个人大喊: ‘我的名字是乐乐1号’。因为他没有在自己的独立办公室里(函数内部),也没有经过门禁验证(new 关键字),系统根本不知道他是谁,读不出他的身份。
此时,系统默认他是在向大老板(全局对象 window)汇报。结果就是,大老板的名字被强行改成了‘乐乐’(window.name = '乐乐')。
这还不是最糟的。如果走廊里另一个人也跑出来大喊: ‘我的名字是欢欢1号’,大老板的名字又被改成了‘欢欢’。最后全公司系统查大老板名字,发现一会儿是乐乐,一会儿是欢欢,原本的大老板名字彻底丢了——这就是全局变量污染,往往是一团乱麻,引发难以排查的Bug。”
补充一个小彩蛋(结合你之前的代码) : 如果你在走廊里大喊 this.name = 123(给名字赋值一个数字),因为浏览器里大老板 window.name 有一个奇葩设定:只接受字符串。系统会强行把数字 123 转成字符串 "123" 贴在大老板脑门上。这种暗地里的类型强制转换,更是让这团乱麻变得诡异至极!
💡 ****为什么会发生这种情况?(回顾之前的知识点)
还记得我们之前讨论过的 “ 私有地盘绝不共享” 吗?
- 当你在 函数内部(如 function Agent1 或 constructor)写 this.name = name 时,因为有 new 关键字帮忙,new 会创建一个全新的空对象,并让 this 指向这个新对象。所以 this.name 成了实例的私有属性。
- 当你在 函数外面 写 this.name 时,没有任何人(比如 new)帮你创建私有地盘。此时 this 只能“无家可归”,默认攀附到最顶级的“大老板”——全局对象(window / global)办公区。于是,你就无意间在大老板的公共墙上乱涂乱画,污染了全局变量。
总结: 在函数外面直接写 this.xxx = ... 是一种极其不规范、危险且应该避免的写法,它会造成全局变量污染,引发难以排查的 Bug。这就是为什么现代 JavaScript 开发强烈推荐始终使用 class 或 new 来管理 this,或者在文件开头加上 "use strict" 来防止 this 指向全局。
所以,原生JS铁律: this必须活在函数里,只有在函数被调用那一刻,由“谁用的它”,来决定这个this是谁。
2、 Vue 里的 this:被总经理“绑定”过的特工
3、 我们在 Vue 里写代码时,经常这样写:
你是否会觉得 this.name 仿佛随时都能用,不用管谁调用的,它好像自动就知道要去读 data 里的 name。
为什么?因为 Vue 的底层是个超级贴心的总经理!
当 Vue 执行 new Vue() 启动莱莉的大脑时,总经理做了两件大事:
1. 把 data 里的所有数据(name、age),全部搬到了当前 Vue 实例的私人信息表上(挂到了 this 上)。
2. 把 methods 里的所有方法,偷偷用原生 JS 的 .bind(this) 技术,永久绑定了“本人”的身份!
这个 .bind(vm) 就像给特工发了一张永久身份卡,无论这个方法被谁拿去用,无论在哪里调用,它内部的 this 永远死死锁定为 vm(莱莉的 Vue 实例)。
一句话点破: 你在 Vue 里觉得 this 好用,是因为 Vue 做了“保姆级”的绑定工作;现在我们学原生源码,就是要把这层外衣脱掉,看看底层那个原始的、根据调用者动态变化的 this 是怎么运作的。
理解了这一层,以后你看 Vue 源码里那些 this.xxx,就不会再觉得是魔法,而是清楚地知道:哦,这是某个特工在自己的工位上,读取自己私人信息表上的档案!