原型理解从入门到精通

原型这块知识点,其实在我们工作中的使用非常简单。但是俗话说"面试造火箭,工作拧螺丝",在面试中,面试官不时就会考察一些花里胡哨的问题,所以,我们只有将这个概念和他的相关知识理解透彻,才能以不变应万变。

两个容易混淆但要分清的东西

  1. 每个普通对象都有内部隐式属性 ​[[Prototype]]​​ (常见访问名 ​proto ------ 它指向另一个对象(即原型对象)。

    所以原型对象名字的由来就是,一个对象有一个 prototype 属性,就是原型属性,而这个原型属性本身又是一个对象,所以称之为原型对象。

  2. 函数(作为构造函数)有 ​.prototype​ 属性 ------ 当你用 new Fn() 创建实例时,实例的 [[Prototype]] 会被设置为 Fn.prototype

总结:.prototype 是构造函数的属性;[[Prototype]]/__proto__ 是普通对象实例的内部指针,二者在构造/实例化时建立联系,但不是同一个东西。

原型链:属性查找的核心机制

当你访问 obj.prop 时,JS 的查找流程如下:

  1. 先查看 obj 自身是否有名为 prop自有属性。有就返回。
  2. 没有则沿着 obj.[[Prototype]](即 obj.__proto__)去找,找到就返回。
  3. 若仍未找到则继续沿着原型的 [[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)

  1. 新建一个空对象 obj
  2. 这个空对象的 [[Prototype]] 被设置为 F.prototype
  3. 执行 F,并把 this 指向 obj
  4. 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
}

hasOwnPropertyinObject.keys 的区别

  • obj.hasOwnProperty('a'):只检查自身属性(不走原型链)。
  • 'a' in obj:检查自身或原型链上是否存在属性(包括不可枚举的)。
  • Object.keys(obj) / for...inObject.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.prototypeArray.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 自己也是一个函数,它也是自己构造出来的。这就像是先有鸡还是先有蛋的问题 😂。

相关推荐
Heo2 小时前
通用会话控制方案
前端·javascript·后端
用户21411832636022 小时前
Claude Skills 实战指南:一键生成公众号封面,3D 插画 + 描边标题 3 秒出图
后端
Heo2 小时前
跨域问题解决方案汇总
前端·javascript·后端
武子康2 小时前
大数据-154 Apache Druid 架构与组件职责全解析 版本架构:Coordinator/Overlord/Historical 实战
大数据·后端·apache
ZZHHWW2 小时前
RocketMQ vs Kafka04 - 高性能设计与调优
后端
Yuroo zhou2 小时前
石油钻井、HDD、采矿:不同工况下,如何抉择您的陀螺定向短节?
前端·科技·硬件架构·钻井·采矿
q***46522 小时前
Spring Boot(七):Swagger 接口文档
java·spring boot·后端
shmily麻瓜小菜鸡2 小时前
Element Plus 的 <el-table> 怎么点击请求后端接口 tableData 进行排序而不是网络断开之后还可以自己排序
前端·javascript·vue.js
ZZHHWW2 小时前
RocketMQ vs Kafka03 - 高可用机制深度剖析
后端