原型这块知识点,其实在我们工作中的使用非常简单。但是俗话说"面试造火箭,工作拧螺丝",在面试中,面试官不时就会考察一些花里胡哨的问题,所以,我们只有将这个概念和他的相关知识理解透彻,才能以不变应万变。
两个容易混淆但要分清的东西
-
每个普通对象都有内部隐式属性
[[Prototype]] (常见访问名 proto) ------ 它指向另一个对象(即原型对象)。所以原型对象名字的由来就是,一个对象有一个 prototype 属性,就是原型属性,而这个原型属性本身又是一个对象,所以称之为原型对象。
-
函数(作为构造函数)有
.prototype 属性 ------ 当你用new Fn()创建实例时,实例的[[Prototype]]会被设置为Fn.prototype。
总结:
.prototype是构造函数的属性;[[Prototype]]/__proto__是普通对象实例的内部指针,二者在构造/实例化时建立联系,但不是同一个东西。
原型链:属性查找的核心机制
当你访问 obj.prop 时,JS 的查找流程如下:
- 先查看
obj自身是否有名为prop的自有属性。有就返回。 - 没有则沿着
obj.[[Prototype]](即obj.__proto__)去找,找到就返回。 - 若仍未找到则继续沿着原型的
[[Prototype]](形成链)向上查找,直到null(查不到返回undefined)。
这就是所谓的 **原型链(prototype chain)**。
ES6
class是语法糖,本质仍用原型。
示例:
JavaScript
const grand = { greet() { return 'hi from grand'; } };
const parent = Object.create(grand);
parent.say = () => 'parent';
const child = Object.create(parent);
child.own = 1;
console.log(child.own); // 1 (own property)
console.log(child.say()); // 'parent' (从 parent 找到)
console.log(child.greet()); // 'hi from grand' (从 grand 找到)
console.log(Object.getPrototypeOf(child)); // parent
我们既可以通过构造函数的方式实现继承,也可以通过纯原型继承(Object.create())的方式实现。
Object.getPrototypeOf(obj):安全地获取[[Prototype]]。Object.setPrototypeOf(obj, proto):设置对象的原型。通常优先建议使用Object.create在创建时设置原型。
构造函数与 new 的工作原理
当你写 new F(arg):
- 新建一个空对象
obj。 - 这个空对象的
[[Prototype]]被设置为F.prototype。 - 执行
F,并把this指向obj。 - 若
F返回对象,则最终结果为该对象;否则返回obj。
因此,F.prototype 是实例继承的方法/属性的来源。
JavaScript
/**
* 模拟实现 new 操作符的函数
* @param {Function} Constructor 构造函数
* @param {...any} args 传递给构造函数的参数
* @return {*} 如果无返回值或者显示返回一个对象,则返回构造函数的执行结果;如果显示返回一个基本类型,则返回构造函数的实例
*/
function myNew(Constructor, ...args) {
// 1. 创建一个全新的空对象 2. 为这个空对象设置原型(__proto__)
// 可以使用 {},但是推荐使用 Object.create() 创建对象并设置原型
const instance = Object.create(Constructor.prototype)
// 3. 绑定构造函数的this为其新创建的空实例对象,并执行构造函数体
const result = Constructor.apply(instance, args)
const isObject = typeof result === 'object' && result !== null
const isFunction = typeof result === 'function'
// 4. 如果构造函数返回一个非原始值,则返回这个对象;否则返回创建的新实例对象
if (isObject || isFunction) return result
return instance
}
hasOwnProperty、in、Object.keys 的区别
obj.hasOwnProperty('a'):只检查自身属性(不走原型链)。'a' in obj:检查自身或原型链上是否存在属性(包括不可枚举的)。Object.keys(obj)/for...in:Object.keys返回自身可枚举属性数组;for...in会枚举自身 + 可枚举的继承属性(可用hasOwnProperty过滤)。
示例:
JavaScript
const p = {x:1};
const o = Object.create(p);
o.y = 2;
'x' in o // true
o.hasOwnProperty('x') // false
Object.keys(o) // ['y']
for (const k in o) { console.log(k); } // 'y' 'x'
instanceof 如何工作
obj instanceof Constructor 检查的是 Constructor.prototype 是否出现在 obj 的原型链上(通过 Object.getPrototypeOf 递归判断)。
TypeScript
/**
* 模拟 instanceOf 的实现
* @param object 实例对象
* @param Constructor 构造函数(类)
* @return {boolean}
*/
function myInstanceOf(object, Constructor) {
// 初始获取对象的原型
let proto = Object.getPrototypeOf(object)
while (true) {
// 遍历到原型链顶端
if (proto === null) return false
// 找到匹配的原型
if (proto === Constructor.prototype) return true
// 继续向上查找原型链
proto = Object.getPrototypeOf(proto)
}
}
覆盖与读取顺序
如果对象自身有同名属性,会遮蔽原型上的同名属性:
JavaScript
const proto = {v:1};
const o = Object.create(proto);
o.v = 2;
console.log(o.v); // 2 (自身属性优先)
delete o.v;
console.log(o.v); // 1 (回退到原型)
修改原型
你可以给原型添加/修改方法,所有继承该原型的对象都会受影响:
JavaScript
Array.prototype.myLog = function(){ console.log(this.length); };
[1,2,3].myLog(); // 3
注意:
- 不要随意修改内置对象(如
Object.prototype、Array.prototype)。修改 prototype 会影响所有实例,可能引入难以追踪的副作用。 这也是非常常见的一种网络安全漏洞:原型污染。指攻击者使用某种
单独说说 constructor
上面的内容看起来是不是还挺简单的。如果上面内容已经完全理解了,那么再来看 construtor 属性。
JavaScript 每个函数(构造函数)对象天生都会有一个 prototype 属性,而这个 prototype 对象中,默认会有一个指向函数本身的属性 ------ constructor。
可以理解为:
constructor 是原型对象上一个指针,用来指向创建该实例的构造函数。
JavaScript
function Person(name) { this.name = name; }
console.log(Person.prototype.constructor === Person); // true
上述这段代码还比较好理解,总之就是 prototype 这个对象身上有一个属性叫做 constructor,这个 constructor 刚好指向原 构造函数。
接着这段代码的思路,我们再来看看下面这段代码:
JavaScript
function Person(name) { this.name = name; }
const p = new Person("Tom");
console.log(p.constructor === Person); // true
诶?不儿?constructor 不是 prototype 上的属性吗?实例对象上也有这个属性吗?
如果你能想到这里,那说明之前的内容至少你已经学懂了。接下来让我告诉你为什么 p.constructor === Person?
原因其实也很简单,因为:
JavaScript
p.constructor
= p.__proto__.constructor // 实例上没有 constructor,会去原型 __proto__ 查找
= Person.prototype.constructor
= Person
为什么上面的继承方式我没有说 constructor?
因为原型重写后会丢失 constructor 指向,需要手动补回。看这段代码:
JavaScript
function Animal() {}
Animal.prototype = {
eat() {}
};
乍眼一看,我们是为 Animal 构造函数添加了 eat 方法,但其实 ⚠️ 这样做会 覆盖原始默认的 prototype 对象 ,从而导致 constructor 丢失(变成 Object ==> { eat(){} } )。
JavaScript
console.log(Animal.prototype.constructor); // 此时是 Object,不是 Animal
所以,如果你非要这么写,还得自己补回 constructor:
JavaScript
function Animal() {}
Animal.prototype = {
constructor: Animal, // 手动补回构造函数
eat() {}
};
这样你是不是明白了,为什么上面的继承方式我没有说 constructor。不是不行,而是不太推荐。任何人都可以随意改原型 ,导致 constructor 变得不可信。
ES6 class 的
constructor本质也是一样的。
我是真的不想再谈 Funciton 了
这一节完全可以不看,因为本质上还是上面的内容,但奈何总有面试官喜欢挖坑,也总有同学喜欢上当~
普通函数(非箭头)天然可以作为构造函数。所以上面说的什么 Object、Person 等等所有函数都是 Function 的实例。
JavaScript
console.log(Person.__proto__ === Function.prototype) // true
console.log(Object.__proto__ === Function.prototype) // true
console.log(Function.__proto__ === Function.prototype) // true
Function.prototype 自身也是一个函数(内置),它的 prototype 与普通对象不同------记住 Function 本身是一个 constructor:
JavaScript
Function instanceof Function // true
Function.prototype instanceof Function // false (Function.prototype 是个普通函数对象)
Function.prototype.__proto__ === Object.prototype // true
JavaScript
Person (构造函数)
│
├── prototype → Person.prototype → { constructor: Person, ... } ✅
└── __proto__ → Function.prototype ✅
JavaScript
Function.__proto__ === Function.prototype // true
Function 自己也是一个函数,它也是自己构造出来的。这就像是先有鸡还是先有蛋的问题 😂。