1. 为什么搞懂了构造函数,你才刚开始理解 JavaScript
大概每个写 JavaScript 的人都有过这么一段经历:用函数写得挺顺手,突然某天看到别人代码里冒出个 new,后面还跟着个首字母大写的函数,顿时有点懵——这函数怎么还能这么用?然后去搜"构造函数",又看到"原型""原型链""prototype""proto"一堆名词堆在一起,越看越晕。
说句实话,这东西初学的时候确实绕,但绕的原因不是因为概念有多难,而是很多教程一开始就把术语倒给你,根本没有把"我们到底在解决什么问题"讲清楚。构造函数和原型这套机制,解决的核心问题非常朴素:我们想要批量创建一堆结构相同、但各自带独立数据的对象,同时这些对象还能共享一部分公共的方法,不至于每个对象都把方法复制一遍。
你想想,如果你要管理一个系统里的十几个用户,每个用户有名字、邮箱、登录状态,还要有"改密码""更新资料"这些动作。如果不用构造函数,你得手写十几个对象,还得把方法在每个对象里都写一遍,或者用函数去操作外部变量,封装性和可维护性都很难受。
构造函数和原型,就是 JavaScript 在还没有 class 的年代里给出的答案。它跟你熟悉的"面向对象"不完全一样:没有"类"这种模板,而是用普通函数充当构造器,用原型对象来做方法共享。后来 ES6 加了 class 语法,但说白了那只是语法糖,底层依旧是构造函数和原型在撑着。所以你去看很多源码、框架知识、甚至是面试题,绕不开这一块。
这篇文章我就从实际使用的角度,把构造函数和原型串一遍,包括 new 的过程、prototype 和 __proto__ 的关系、原型链查找机制、以及用原型做继承的各种姿势。最后再聊聊我在项目里实际踩过的坑——这些坑不踩一遍,光看文档真的很难理解为什么大家总强调"别乱改内置对象的原型"。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. new 关键字背后那四行代码,藏着整个原型机制的入口
2.1 构造函数本质上就是个普通函数,区别只在调用方式
在 JavaScript 里,构造函数并不是一种特殊的语法结构。你声明一个函数,用 new 去调用它,它就成了构造函数;直接调用,它就是普通函数。
javascript复制function User(name, email) {
this.name = name;
this.email = email;
this.loggedIn = false;
}
// 用 new 调用:作为构造函数
const u1 = new User('张三', 'zhangsan@example.com');
// 直接调用:普通函数,this 指向 window/undefined(取决于是否严格模式)
const u2 = User('李四', 'lisi@example.com');
你去看 u1,它是一个对象,有自己的 name、email、loggedIn 属性。而 u2 是 undefined,因为 User 函数里没有任何 return 语句——注意,普通函数没有返回值时默认返回 undefined,这一点无论在哪种语言里都一样。
所以从使用层面看,构造函数和普通函数的唯一区别就是:你用没用 new 去调它。
这个看似简单的区别,背后隐藏着 JavaScript 设计者对对象创建机制的全部安排。当 new 被调用时,引擎会偷偷干四件事,这四件事是理解后面所有原型概念的关键。
2.2 new 逐步拆解:从空对象到原型关联
执行 new User('张三', 'zhangsan@example.com') 时,JavaScript 引擎大致做了以下操作:
- 创建一个全新的空对象;
- 把这个空对象的
__proto__(内部原型指针)指向User.prototype; - 让
User函数内部的this指向这个新对象,并执行函数体; - 如果函数没有返回对象类型的值,
new表达式的返回值就是第一步创建的那个对象。
对应到代码,相当于:
javascript复制function myNew(Constructor, ...args) {
// 第一步:创建空对象
const obj = {};
// 第二步:关联原型
Object.setPrototypeOf(obj, Constructor.prototype);
// 第三步:绑定 this 并执行
const result = Constructor.apply(obj, args);
// 第四步:根据返回值决定结果
return (result !== null && typeof result === 'object') || typeof result === 'function'
? result
: obj;
}
这一步特别关键,因为很多人一直想不通:为什么 this.name = name 之后,u1 身上就有 name 属性了?答案就在第三步——this 被指向了那个新对象,然后你把属性挂到了 this 上,就相当于挂到了新对象上。
而第二步更重要。Object.setPrototypeOf(obj, Constructor.prototype) 做的关联,让新对象跟 User.prototype 之间建立了一条"隐式链接"。这条链接,就是之后所有原型链查找的基础。没有这条链接,方法共享和属性继承都无从谈起。
那你可能在问了:User.prototype 是个什么东西?它怎么就自动存在了?这里面有 JavaScript 的一个设计巧思:每个函数天生自带一个 prototype 属性,它指向一个对象,这个对象里有且仅有一个不可枚举的属性 constructor,指向函数自身。
javascript复制function User(name) {
this.name = name;
}
console.log(User.prototype.constructor === User); // true
你可以把 User.prototype 理解为一个"公共挂载点"。所有通过 new User 创建出来的对象,都能顺着 __proto__ 找到这个挂载点,从而访问挂载点上定义的方法和属性。
2.3 给原型挂方法:为什么不能直接写箭头函数
了解了 prototype 是公共挂载点之后,最常见的用法就是往上面放方法。
javascript复制function User(name, email) {
this.name = name;
this.email = email;
this.loggedIn = false;
}
User.prototype.login = function () {
this.loggedIn = true;
console.log(`${this.name} 登录成功`);
};
User.prototype.logout = function () {
this.loggedIn = false;
console.log(`${this.name} 已退出`);
};
const u1 = new User('张三', 'zhangsan@example.com');
const u2 = new User('李四', 'lisi@example.com');
u1.login(); // 张三 登录成功
console.log(u2.loggedIn); // false,u2 没有被影响
这里有两个关键点需要强调:
第一,login 和 logout 方法只存在于 User.prototype 上,但 u1 和 u2 都能直接调用。这不是因为每个对象都拷贝了这些方法,而是因为对象在查找属性时,会顺着原型链一层一层往上找,直到找到为止。这种"查找而非复制"的机制,使得 100 个实例对象共用同一个方法,内存开销非常小。
第二,为什么我写的是普通函数 function () {} 而不是箭头函数 () => {}?因为箭头函数没有自己的 this,它在定义时捕获外层作用域的 this。如果你把 login 写成箭头函数,里面的 this 指向的是定义那一刻的外层环境(很可能是 undefined 或全局对象),那 this.loggedIn = true 就完全失效了。这一点在实际开发里经常有人踩坑,尤其是刚从 React 类组件时代过来的人,习惯性地用箭头函数,结果方法里的 this 彻底乱了。
提示:构造函数内部的方法、原型上的方法,都建议用普通函数声明。只有当你明确不需要访问实例的
this时,才考虑箭头函数。
3. 原型链不是复制链,而是一条"查找链"
3.1 一次属性访问,到底在背后经历了什么
理解了 prototype 是公共挂载点,下一步就必须理解原型链的查找机制。JavaScript 中几乎所有的对象都有一个内部属性 [[Prototype]],在浏览器里通常暴露为 __proto__。它指向创建该对象时关联的原型对象。
看这段代码:
javascript复制function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function () {
console.log(`${this.name} 发出声音`);
};
const dog = new Animal('旺财');
console.log(dog.name); // 旺财,来自 dog 自身
console.log(dog.speak); // 函数,来自 dog.__proto__ 也就是 Animal.prototype
console.log(dog.toString); // 函数,来自 Object.prototype
当你访问 dog.speak 时,引擎做的不是简单的"从对象身上取属性",而是执行了一次链式查找:
- 先在
dog自身属性里找speak,找不到; - 顺着
dog.__proto__(即Animal.prototype)找,找到了,返回; - 如果
Animal.prototype上也没有,就继续找Animal.prototype.__proto__,也就是Object.prototype; - 如果
Object.prototype上也没有,返回undefined。
这才是"原型链"这三个字的准确含义:**它不是把父级的方法复制一份给子级,而是形成了一条从当前对象出发、逐级向上查找的链。**对象的方法真正存储的位置,始终在原型链上某一层,而不是在每个实例身上。
用生活化的类比来说:你不需要给每个员工都发一份公司规章制度手册,你只需要保证公司有这份手册,员工遇到问题时知道去哪个位置查阅就行。原型链就是这条"查阅路径"。
3.2 每次 new 都会创建一个原型关联,不会创建方法副本
有时候面试里会问:"new 了 100 个对象,每个对象都有方法吗?"很多人会答错成"每个对象都有一份方法副本"。实际不是这样。
每个 new 出来的对象,只有构造函数里通过 this.xxx = xxx 赋值的属性是"属于自己的"。原型上的方法只有一份,存在于构造函数的 prototype 对象上,100 个实例共享同一份。
验证方式也很简单:
javascript复制function User(name) {
this.name = name;
}
User.prototype.sayHi = function () {
console.log(`Hi, ${this.name}`);
};
const a = new User('A');
const b = new User('B');
console.log(a.sayHi === b.sayHi); // true,同一个函数引用
console.log(a.hasOwnProperty('name')); // true
console.log(a.hasOwnProperty('sayHi')); // false,sayHi 不在实例自身上
记住 hasOwnProperty 这个方法,它在实际开发中非常有用,可以用它区分"属性是对象自身的,还是从原型链上继承来的"。
3.3 proto、prototype 和 constructor 的三角关系
这是初学者最容易搞混的一组关系。我用一个表格把它们梳理清楚:
| 属性 | 谁身上有 | 指向什么 | 作用 |
|---|---|---|---|
prototype |
函数 | 一个对象 | 作为通过 new 创建出来的实例的原型对象 |
__proto__ |
几乎所有对象 | 创建该对象时的原型对象 | 构成原型链的实际通道 |
constructor |
函数原型对象上的属性 | 函数自身 | 反向标识对象是由哪个函数构造出来的 |
具体到代码:
javascript复制function Person(name) {
this.name = name;
}
const p = new Person('小明');
console.log(p.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true
console.log(p.constructor === Person); // true,因为 p 自身没有 constructor,顺着原型链找到了 Person.prototype.constructor
console.log(Person.prototype.__proto__ === Object.prototype); // true,再往上走一层
这组关系理解透了,你就能解释很多"莫名其妙"的现象。比如为什么 p.constructor 能拿到 Person,因为 p 自己身上没有 constructor,但原型链上有。再比如为什么有些库要把 prototype.constructor 指回来,因为继承时如果把 Child.prototype 整个覆盖成 new Parent(),constructor 就丢了,指向了 Parent,这时候需要手动把 Child.prototype.constructor 重新指回 Child。
3.4 原型链到顶是什么?为什么 toString 大家都在用但不会冲突
原型链的顶端是 Object.prototype,再往上就是 null。很多人会问:那 null 是什么?它相当于链条的终点标志,表示"到这里就再也没有原型可找了"。
javascript复制console.log(Object.prototype.__proto__); // null
这也解释了为什么所有对象都有 toString、hasOwnProperty、valueOf 这些方法——它们都定义在 Object.prototype 上,所有对象都能通过原型链访问到。
但有个细节需要注意:虽然所有对象都能调用 toString,但如果你在自定义构造函数里给原型定义了同名方法,调用链会优先命中更接近实例的那一层。这种"近的优先"就是原型链查找的自然行为,也给"方法覆盖"和"多态"提供了基础。
在实际工作中,我经常用 Object.prototype.toString.call(x) 来判断某个变量的精确类型,因为 x.toString() 可能被实例自己的方法覆盖,而直接调用 Object.prototype 上的版本可以绕开覆盖:
javascript复制const arr = [1, 2, 3];
console.log(Object.prototype.toString.call(arr)); // [object Array]
4. 用原型做继承:从手写 extends 到 class 语法糖
4.1 为什么需要继承,以及原型继承到底"继承"了什么
构造函数解决了"批量创建同类对象"的问题,但真实项目里对象之间往往有层级关系。比如一个系统里有"管理员"和"普通用户",两者都是用户,但是管理员额外有封禁用户、审核内容的权限。如果每个角色都重新实现一套用户逻辑,代码冗余会非常大。
继承就是想实现一句话:子类型拥有父类型的属性和行为,同时还能扩展自己的独有行为。
JavaScript 没有传统语言里的"类继承",它是通过原型链来实现继承的。最常见的做法是"原型链继承 + 构造函数窃取"的组合。
4.2 经典组合继承:原型链 + 构造函数一起用
看这段代码:
javascript复制function User(name, email) {
this.name = name;
this.email = email;
}
User.prototype.login = function () {
console.log(`${this.name} 登录成功`);
};
function Admin(name, email, permissions) {
// 借用父构造函数,保证 name 和 email 被设置为实例自身的属性
User.call(this, name, email);
this.permissions = permissions;
}
// 设置 Admin.prototype 的原型为 User.prototype
Admin.prototype = Object.create(User.prototype);
Admin.prototype.constructor = Admin;
Admin.prototype.banUser = function (userId) {
console.log(`${this.name} 封禁了用户 ${userId}`);
};
const admin = new Admin('管理员', 'admin@example.com', ['ban', 'review']);
admin.login(); // 管理员 登录成功,方法来自 User.prototype
admin.banUser(1001); // 管理员 封禁了用户 1001,方法来自 Admin.prototype
这里做了三件比较关键的事情:
第一,在 Admin 构造函数内部用 User.call(this, name, email) 调用了父构造函数。这样做的目的是让父构造函数里通过 this.xxx 设置的那些属性,直接设置到子类的实例上。
第二,用 Object.create(User.prototype) 创建了一个新对象,并把这个对象作为 Admin.prototype。这一步很关键:如果你直接写 Admin.prototype = User.prototype,那后面给 Admin.prototype 添加 banUser 方法时,会污染 User.prototype,导致所有普通用户也都有了 banUser。Object.create 的作用是创建一个原型指向 User.prototype 的新对象,这样修改 Admin.prototype 不会影响到 User.prototype,同时又保证了 admin 可以通过原型链找到 User.prototype 上的方法。
第三,手动把 Admin.prototype.constructor 指回 Admin。因为 Object.create 生成的新对象没有 constructor 属性,它只能从原型链上继承到 User.prototype.constructor(也就是 User),这会让你在打印 admin.constructor 时得到 User,造成误导。
这是 ES5 时代最经典的标准做法。虽然代码看起来有些繁琐,但它把原型链继承的每个环节都暴露出来了,这也是为什么我建议新手先别急着上 class,把这个版本写一遍,你才能真正理解 class 背后发生了什么。
4.3 class 语法糖:底层还是原型链
ES6 引入的 class 语法,本质上是把上面这套组合继承封装成了更易读的语法:
javascript复制class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
login() {
console.log(`${this.name} 登录成功`);
}
}
class Admin extends User {
constructor(name, email, permissions) {
super(name, email);
this.permissions = permissions;
}
banUser(userId) {
console.log(`${this.name} 封禁了用户 ${userId}`);
}
}
const admin = new Admin('管理员', 'admin@example.com', ['ban', 'review']);
admin.login(); // 管理员 登录成功
extends 关键字内部做的事情,跟 Object.create 那套逻辑是等价的。super() 就相当于 User.call(this, ...)。class 里声明的方法,全部会被放到 prototype 上。从这个角度看,class 并没有改变 JavaScript 的继承模型,只是让代码更接近其他语言的写法。
对于项目使用,我个人的建议是:**新代码尽量用 class,但必须理解它底层是原型链。**因为你要调试复杂的调用链、排查 this 指向问题、分析第三方库的继承关系时,最终还是要回到原型链这个底层视角来看问题。
4.4 实例属性和原型属性的取舍
在实际编码中,有一个最常见的问题:哪些属性应该放在构造函数里,哪些应该放在原型上?
规则其实很简单:
- 每个实例都需要有独立值的属性,放在构造函数里通过
this.xxx = xxx定义。比如用户的name、email、id,每个用户都不一样,必须每个实例自己保存一份。 - 所有实例共享、且不会复制的属性和方法,放在原型上。比如
login、logout、formatProfile这些方法,以及一些不会变的常量。
但这里有个容易翻车的点:如果你在构造函数里定义数组或对象类型的属性,每个实例会各自持有一份独立的引用,互不影响。这也正是大家推荐在构造函数里定义复杂类型属性的原因。但如果你放在原型上,那所有实例会共享同一个数组/对象,改一个等于改全部。
javascript复制function Team(name) {
this.name = name;
this.members = []; // 每个实例独立持有自己的 members
}
const teamA = new Team('A组');
const teamB = new Team('B组');
teamA.members.push('张三');
console.log(teamB.members); // [],不受影响
如果你把 members 放到原型上:
javascript复制function Team(name) {
this.name = name;
}
Team.prototype.members = [];
const teamA = new Team('A组');
const teamB = new Team('B组');
teamA.members.push('张三');
console.log(teamB.members); // ['张三'],所有实例共享同一个数组
这个坑我在真实项目中遇到过一次。当时一个模块的配置列表放在原型上,结果某个操作往里推了一条数据,别的页面的实例也跟着变了,排查了很久才定位到是原型属性共享引用导致的。
5. 日常开发里最容易被原型绊倒的 5 个场景
5.1 给内置对象原型加方法,可能引发不可预知的连锁反应
新手时期很容易有人告诉你"可以给 Array.prototype 加个方法",比如加一个 sum、加一个 first。理论上确实可以:
javascript复制Array.prototype.first = function () {
return this[0];
};
但实际项目里我很不建议这么做。原因不是技术上行不通,而是维护性和兼容性风险。如果你们团队里其他人引用了某个第三方库,那个库内部也定义了 Array.prototype.first,而且行为跟你定义的不一致,代码中就会出现极其隐晦的 bug——有些地方正常,有些地方返回了错误结果,而且很难从代码层面直接看出是谁在捣乱。
如果团队里多人协作、代码量一上来,对内置原型的修改会变成一种全局状态污染。我个人几乎不在业务代码里修改内置原型的,除非是开发基础工具库,并且经过团队评审。
5.2 instanceof 判断类型,是基于原型链的
很多同学用 instanceof 判断对象类型,但未必意识到它跟原型链的关系。obj instanceof Fn 的本质是:检查 Fn.prototype 是否存在于 obj 的原型链上。
javascript复制function Animal() {}
const a = new Animal();
console.log(a instanceof Animal); // true
console.log(a instanceof Object); // true,Object.prototype 在原型链上
这也解释了奇怪的现象:只要是对象,xxx instanceof Object 都是 true。还有个常见的使用场景,判断数组:
javascript复制const arr = [1, 2, 3];
console.log(arr instanceof Array); // true
但如果你在多个 iframe 或多个 window 环境之间传递对象,instanceof 可能会失效,因为不同的全局对象有不同的 Array.prototype。这种场景条件下,用 Object.prototype.toString.call(arr) 更可靠。
5.3 this 指向问题:原型上的方法被单独拿出来调用时
当你把原型上的方法从对象上解构出来再调用,this 会丢失原来的上下文:
javascript复制function User(name) {
this.name = name;
}
User.prototype.sayHi = function () {
console.log(`Hi, ${this.name}`);
};
const u = new User('小明');
const sayHi = u.sayHi;
sayHi(); // 在严格模式下 this 是 undefined,报错;非严格模式下 this 指向全局对象,输出 Hi, undefined
这类问题在 React 类组件时代非常常见:把方法传入事件监听器时,如果不绑定 this,触发时 this 就丢了。React 的解决方案是绑定或者使用箭头函数,而在原生任务中,如果你恰好用的也是对象方法解耦传递,建议用 Function.prototype.bind 显式绑定:
javascript复制const sayHiBound = u.sayHi.bind(u);
sayHiBound(); // Hi, 小明
5.4 使用 hasOwnProperty 区分"自身属性"和"原型链属性"
实际开发中,遍历对象、判断属性是否存在时,经常需要区分属性到底是实例自己的还是原型链上继承来的。for...in 会把原型链上可枚举的属性也遍历出来,这常常造成意外:
javascript复制function User(name) {
this.name = name;
}
User.prototype.sayHi = function () {};
const u = new User('小明');
for (let key in u) {
console.log(key); // name, sayHi —— sayHi 来自原型,也被遍历到了
}
如果只关心对象自身属性,用 Object.keys(u) 或 u.hasOwnProperty(key) 就能过滤掉原型链上的属性:
javascript复制Object.keys(u); // ['name']
这在写通用工具函数、做数据序列化时特别重要。比如你用 JSON.stringify 序列化一个对象时,原型链上的属性本来就不会被序列化,但如果有人以为 for...in 拿到的都是自身属性,然后把结果塞进接口请求里,就会发出多余字段,引发后端校验错误。
5.5 区分 Object.create(null) 和普通对象的原型
Object.create(null) 创建的对象没有原型,意味着它没有 toString、hasOwnProperty、constructor 这些方法。这种"纯净对象"在某些场景下很有用,比如用作哈希表字典,可以避免意外的原型污染问题。
javascript复制const pure = Object.create(null);
pure.key = 'value';
console.log(pure.key); // value
console.log(pure.toString); // undefined,因为没有原型链
当我前端实现一个需要把用户输入作为 key 的缓存时,我会优先用 Object.create(null),这样更安全,避免 key 名字刚好撞上 __proto__、toString 这种内置属性,戳破原型链。
提示:在排查生产 bug 时,
Object.create(null)这个行为也经常被忽略。有人用{ __proto__: evil }做原型污染攻击,而用Object.create(null)能直接阻断这类攻击面。安全编程时多留个心眼没有坏处。
6. 如果你正打算应聘前端,这些面试题值得记一下
6.1 "new 一个函数,到底发生了什么?"
这是高频面试题。基本回答框架就是上面讲的那四步:创建空对象、关联原型、绑定 this 执行、返回结果。如果能把"如果构造函数返回了一个对象,则 new 表达式的结果就是那个对象"这个边界条件说出来,会显得比较有经验。
javascript复制function Foo() {
this.a = 1;
return { b: 2 };
}
const f = new Foo();
console.log(f.a); // undefined
console.log(f.b); // 2
这正好验证了第四步的判断逻辑:构造函数返回了对象,那么 new 的结果就是那个返回对象,而不是内部创建的实例。
6.2 "constructor 的值一定是构造函数本身吗?"
不一定。当你手动替换了构造函数的 prototype 对象时,constructor 属性可能丢失或被改变。最常见的例子就是用 Object.create 实现继承后没有修复 constructor。
javascript复制function Parent() {}
function Child() {}
Child.prototype = Object.create(Parent.prototype);
// 此时 Child.prototype.constructor 指向 Parent,而非 Child
const c = new Child();
console.log(c.constructor); // Parent,期望值应该是 Child
所以业界惯例是:
javascript复制Child.prototype.constructor = Child;
6.3 "手写一个 Object.create" 怎么实现?
Object.create(proto) 的作用是创建一个新对象,并把它的原型指向 proto。手写版本的思路就是利用一个临时构造函数:
javascript复制function create(proto) {
function F() {}
F.prototype = proto;
return new F();
}
这个实现也正好能帮助理解后面很多框架源码里大量出现的"继承辅助函数"是怎么来的。
6.4 "class 的 extends 和 ES5 组合继承有什么差异?"
有了前面章节的铺垫,这个问题就很好答了:两者本质一致,都是通过原型链建立继承关系。差异主要体现在几个方面:
- class 语义上更清晰,必须用
new调用,不能直接作为普通函数调用; extends会正确处理super的调用时机,子类构造函数里必须在访问this之前调用super();- class 里的方法不可枚举,而 ES5 原型上直接赋的方法是可枚举的;
- 原生类(比如
Array、Error)的继承,用 class 比纯 ES5 更稳定。
不过,不管用哪种写法,你只要牢牢抓住"原型链查找机制"这个本质,任何继承题目都不难破解。
7. 最后分享一个判断对象属性的实战技巧
在项目里我们经常要处理接口返回的数据,有时候数据是嵌套对象,有时候可能是 null 或 undefined。判断一个对象身上到底有没有某个属性,我推荐用 in 操作符和 hasOwnProperty 配合起来,理解各自边界:
javascript复制const obj = { a: 1 };
console.log('a' in obj); // true,不管是自身还是原型链上的
console.log(obj.hasOwnProperty('a')); // true,只有自身属性
console.log('toString' in obj); // true,因为原型链上有
console.log(obj.hasOwnProperty('toString')); // false,不是自身属性
如果你要判断"这个属性自身就有且值不是 undefined",最佳方式其实是直接读属性然后看值,或者用 Object.prototype.hasOwnProperty.call(obj, key) 来避开某些对象可能覆盖 hasOwnProperty 的情况:
javascript复制function safeHasOwn(target, key) {
return Object.prototype.hasOwnProperty.call(target, key);
}
这个写法在多端环境(比如浏览器里被重写过的对象、iframe 传过来的对象)里更稳。之前我在一个项目里遇到过一个 bug,就是因为业务数据里某个字段名刚好叫 hasOwnProperty,结果直接调用 obj.hasOwnProperty('xxx') 时,引擎找到的其实是数据字段而不是原来的方法,顿时报错。用 Object.prototype.hasOwnProperty.call 去调用,就能完全避开这类同名覆盖问题。
说实话,构造函数和原型这个东西,不是看一遍就能彻底吸收的,但只要你把它放在实际使用的场景里慢慢磨,遇到一次 this 丢失、遇到一次原型污染问题、调试一次继承链,那些抽象概念就会逐渐落地。我希望这篇文章能帮你搭建起一个大致的框架,然后你在自己的项目里继续去踩、去试、去验证,慢慢它就会内化成你的基本功。
