JS原型与原型链|对象继承与实例底层原理
原型是JavaScript面向对象的底层实现机制。JS没有传统语言的静态类,依靠原型完成属性、方法共享与继承。实例方法复用、class语法、instanceof类型判断全部构建在原型体系之上。
读懂原型与原型链,才能看懂对象实例底层逻辑,分辨各种继承方案优缺点,排查对象属性访问相关bug,是JS从语法基础迈向底层进阶的重要分水岭。
一、原型核心原理概述
每一个函数都自带prototype(显式原型);每一个对象实例都自带__proto__(隐式原型)。实例的 __proto__ 指向构造函数的 prototype。
核心关系:实例.__proto__ === 构造函数.prototype
原型的核心价值:多个实例共用原型上的方法,不需要每个实例复制一份函数,节省内存开销。
// 构造函数 function Person(name) { this.name = name; } // 在原型上挂载共享方法 Person.prototype.sayHi = function() { console.log(`你好,我是${this.name}`) } const p1 = new Person("小明") const p2 = new Person("小红") p1.sayHi() p2.sayHi() // p1 和 p2 使用同一个 sayHi 函数,内存只存一份 console.log(p1.sayHi === p2.sayHi) // true
二、new 关键字底层执行流程
使用 new 创建对象实例,浏览器内部自动执行4步逻辑:
1. 创建一个空普通JS对象 {}
2. 将空对象的 __proto__ 指向构造函数的 prototype
3. 绑定this,执行构造函数内部代码,为新对象添加实例属性
4. 如果构造函数没有手动返回对象类型,则自动返回新建的实例对象
function Car(brand) { this.brand = brand } const car1 = new Car("比亚迪") console.log(car1.__proto__ === Car.prototype) // true
三、原型链查找机制
读取对象属性、方法时会触发原型链向上查找,赋值属性不会触发原型链。
优先在对象自身查找,找到直接返回
自身找不到,沿着 __proto__ 向上访问原型对象
持续向上查找,直到 Object.prototype
Object.prototype.__proto__ 等于 null,到达链尾;整条链找不到返回 undefined
const obj = {} // obj -> Object.prototype -> null console.log(obj.toString) // toString 来自 Object原型
注意:给对象做属性赋值,只会给当前对象新增属性,不会修改原型对象上面的属性。
四、继承实现方案
方案一:原型链继承
将子类构造函数prototype指向父类实例实现继承。
function Animal(type) { this.type = type } Animal.prototype.eat = function() { console.log("动物进食") } function Dog(){} Dog.prototype = new Animal("犬科") Dog.prototype.constructor = Dog // 修正构造器指向 const dog = new Dog() dog.eat()
缺点:父类引用类型属性被全部实例共享;无法向父构造函数传参。
方案二:组合继承(传统工程经典方案)
借用构造函数 + 原型继承结合,解决原型链继承缺陷。
function Animal(type) { this.type = type } Animal.prototype.eat = function() { console.log("动物进食") } function Dog(type) { Animal.call(this, type) // 借用父构造函数生成实例独有属性 } Dog.prototype = new Animal() Dog.prototype.constructor = Dog const dog = new Dog("犬科") console.log(dog.type) dog.eat()
方案三:ES6 class extends 继承
class只是原型的语法糖,底层依旧基于原型机制,简化手写继承代码。
class Animal { constructor(type) { this.type = type } eat() { console.log("动物进食") } } class Dog extends Animal { constructor(type) { super(type) // 调用父类构造函数,必须写在this访问之前 } } const dog = new Dog("犬科") dog.eat()
五、constructor 构造器属性
prototype原型对象自带constructor,默认指向对应的构造函数,用来标记实例由哪个函数创建。
function User(){} const u = new User() console.log(u.constructor === User) // true
开发踩坑:如果直接完整覆盖prototype对象,constructor属性会丢失,需要手动重置。
六、三种类型检测方式与区别
instanceof:沿着原型链做判断,检查构造函数prototype是否出现在实例原型链上,仅适合引用类型。
Object.prototype.toString.call():精准检测JS内置类型,可以区分数组、普通对象等。
typeof:适合原始类型判断;null返回object,无法区分数组与普通对象。
console.log(dog instanceof Dog) // true console.log(Object.prototype.toString.call([])) // "[object Array]" console.log(typeof null) // "object"
七、原型工程实践规范(企业规范)
实例独有的属性写在构造函数this上;多个实例共享的方法挂载prototype,节约内存。
原型上尽量不要存放数组、对象这类引用类型,会出现实例之间互相污染。
class是语法糖,底层依旧原型,不要当成传统静态类。
业务尽量避免直接操作 __proto__;优先使用Object.getPrototypeOf / Object.setPrototypeOf。
八、开发高频避坑总结
prototype属于函数;__proto__属于实例对象,二者不要混淆。
只有读取属性才走原型链,赋值不会向上修改原型。
instanceof结果可以被人为修改原型伪造,不能作为100%可靠类型判断。
原型链层级不要过深,层级过多会增加属性查找性能开销。
class继承子类constructor内部,super必须前置调用,否则this无法初始化直接报错。
九、写在最后
原型与原型链是JavaScript面向对象底层根基。JS依靠原型实现属性共享与继承,class、实例、instanceof全部建立这套体系之上。
弄懂原型链,才能够读懂内置对象方法、各类继承写法、框架组件继承逻辑,突破JavaScript底层进阶门槛。