原型理解从入门到精通

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

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

  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 自己也是一个函数,它也是自己构造出来的。这就像是先有鸡还是先有蛋的问题 😂。

相关推荐
GetcharZp25 分钟前
玩转 Linux 机器视觉:手把手带你搞定 Ubuntu 下海康工业相机 C++ SDK
后端
橙子家1 小时前
浏览器缓存之【基础键值存储】:Local storage 和 Session storage
前端
星星在线4 小时前
MusicFree:一个「All in One」的个人音乐服务器,让听歌回归简单
前端·后端
IT_陈寒5 小时前
Redis的SETNX并发问题让我加了三天班
前端·人工智能·后端
demo007x5 小时前
Docling 文档转换以及技术架构分析
前端·后端·程序员
京东云开发者6 小时前
京东市民服务又“上新”!这次是黑龙江“龙易办”
前端
袋鱼不重6 小时前
我的神奇同事,AI 用多了居然写了个 Open In Codex
前端·后端·ai编程
用户8356290780516 小时前
使用 Python 操作 Word 内容控件
后端·python
像我这样帅的人丶你还6 小时前
啥? 前端也要会干Java?🛵🛵🛵
后端
竹林8186 小时前
Web3表单签名验证:我用 wagmi 和 ethers 给 DApp 加了一个“免密登录”,踩坑记录全在这了
javascript