JavaScript手录17-原型
一、原型
原型
- 原型的本质:原型(prototype)是JavaScript中实现继承的核心机制,是专门存储由某一方法(构造函数)创建的所有对象实例共有属性的存储空间。
- 原型就像是“公共仓库”,同一构造函数所创建的所有实例都能共享仓库中的属性和方法,避免重复储存,节省内存。
- 理解:原型本身是一个对象,属于构造函数的属性(
prototype
),并不是一种独立概念。
方法与对象的关系
- 方法的本质:方法即函数(function),可以通过返回对象字面量创建对象实例。
- 示例:
function createPerson() {return { name: '小明' }
}
let person = createPerson() // 由方法创建对象实例
- 对象的原型属性:所有通过方法创建的对象都包含[[prototype]](隐式原型)属性。该属性指向对象的原型,用于访问共享属性。
- 查看[[prototype]]属性:展开对象即可在控制台中查看[[prototype]]属性。
构造函数
- 普通方法创建对象的局限性:类似上文中示例那样创建对象的普通方法,每次调用只能创建独立对象,无法实现属性共享和方法复用,在需要创建大量相似对象的场景时,创建效率很低。
- 构造函数:为了提高创建大量相似对象的效率,引入了构造函数。
- 构造函数的作用:作为对象创建的模板,专门用于批量创建结构相同的对象,提高代码复用率,避免重复劳动。
- 适用场景:仅适用于需要创建结构相同而属性值不完全相同的对象。例如:需要创建很多“学生”对象,对象结构均为(name和age)。
构造函数的定义和规范
- 构造函数的本质:构造函数是一种具有特殊用途的函数,但是其本质仍然是函数。构造函数必须通过new关键字调用,并且会隐式返回新对象。
- 构造函数的通用命名规范:首字母大写以同普通函数区分(例如:Student、Person、Animal)。尽量与JS内置函数(例如:Date、Array、Math)保持一致,提升代码可读性。
- 构造函数的分类:
- JS内置构造函数:JS内置,例如Date()函数,创建日期对象;Array()函数创建数组;
- 自定义构造函数:当JS内置的构造函数无法满足需求时,可以创建自定义构造函数使用。
- 自定义构造函数示例:
// 创建具有name和age属性的student对象的构造函数
// 构造函数定义
function Student(name, age){// 给实例添加属性this.name = namethis.age = age
}
// 使用new关键字创建实例
let stu1 = new Student('小明', 20)
let stu2 = new Student('小红', 20)
// 注:由构造函数创建的对象称为构造函数的实例
new关键字的底层原理
- new关键字的核心作用:new是运算符,用于通过构造函数创建对象实例。“有new就一定有对象”。\
- new关键字与构造函数:构造函数必须使用new关键字调用,否则无法创建对象实例,并且this会指向全局(例如浏览器中的window)而不是自身。(对象实例是由new创建的,而不是构造函数return的结果。构造函数可以理解为用于给想要创建的对象实例构造一个结构。)
- new关键字的底层原理:
- 创建空对象:在内存中创建一个空对象。
- 建立原型链:将实例的隐式原型(
<font style="color:rgb(0, 0, 0);">[[Prototype]]</font>
)指向构造函数的<font style="color:rgb(0, 0, 0);">prototype</font>
(建立原型链)。 - 添加属性:将构造函数的
<font style="color:rgb(0, 0, 0);">this</font>
指向这个实例,执行构造函数为实例添加属性。 - 返回对象:自动返回新对象(即使构造函数没有return语句)。
- 手动实现new的示例:
// 定义一个构造函数
function Person(name, age){this.name = namethis.age = age// 测试返回值情况// return { gender: 'male' };// 如果构造函数有返回对象,myNew会优先使用该对象
}// 手动实现new的功能// ...args用于接收动态参数(即不确定数量的参数;例如构造函数可能有2个参数,也可能有20个参数)
function myNew(constructor, ...args){// 1.创建空对象(实例)let obj = {}// 2.将实例的隐式原型指向构造函数的原型prototype(建立原型链)Object.setPrototypeOf(obj, constructor.prototype)// 等价于: obj.__proto__ = constructor.prototype 但是不推荐直接操作__proto__// 3.调用构造函数,将this指向实例,并传递参数let res = constructor.apply(obj, args)// 4.判断构造函数返回值:如果返回对象则使用返回值;否则返回实例return res instanceof Object ? res : obj}// 用原生的new创建实例
let p1 = new Person('张三', 20)
let p2 = myNew(Person, '李四', 22)// 测试结果
console.log(p1)
console.log(p2)// 原型上的共有方法
Person.prototype.sayHello = function(){return `我是${this.name},我今年${this.age}岁了`
}console.log(p1.sayHello())
console.log(p2.sayHello())// 验证原型关联是否正确
console.log(p1.__proto__ === Person.prototype) // true
console.log(p2.__proto__ === Person.prototype) // true
自有属性与共有属性
类型 | 定义 | 创建方式 | 存储位置 | 特点 |
---|---|---|---|---|
自有属性 | 实例独有的属性 | 通过构造函数内 this.***添加;或者在实例上直接添加 | 实例对象本身 | 仅影响当前实例;访问时优先级高于共有属性 |
共有属性 | 所有实例共享的属性 | 必须添加到构造函数的prototype上 | 构造函数的prototype | 修改后影响所有实例;通过原型链访问 |
示例:
function Student(name, age){this.name = namethis.age = age
}
let xm = new Student('小明', 20)
let xh = new Student('小红', 21)
// 共有属性
Student.prototype.car = 'bmw'
// 小明有自由car;小红没有
xm.car = 'benz' // 自由属性
console.log(xm.car) // 'benz' 来自实例添加到自有属性;自有属性优先访问
console.log(xh.car) // 'bmw' 来自prototype上的共有属性
补充:判断自有属性的方式
// 判断方法:使用obj.hasOwnProperty(prop),返回true为自有属性,false为共有属性或不存在。
原型链与属性访问机制
- 原型链的定义:实例的[[prototype]]指向构造函数的prototype,而prototype本身也是对象,其[[prototype]]指向更上层的原型(例如Object.prototype),形成的链条即原型链
- 属性访问规则:
- 方位属性时,先查找实例自身的自由属性
- 若未找到,沿着原型链向上查找共有属性
- 直到找到Object.prototype(原型链的终点,其[[prototype]]为null);若仍未找到,则返回undefined
二、继承
实现继承是原型链的核心价值。
继承的本质与实现
- 继承的定义:通过原型链机制,使“新类型”复用“现有类型”的属性和方法,并可扩展新特性。
- 核心实现:依赖原型链的链式查找,子类实例的隐式原型指向父类的显示原型
- 示例:学生继承人类的属性,同时扩展自己的方法
// 父类构造函数
function Person(){this.species = '人类'; // 自有属性
}
// 父类共有方法
Person.prototype.eat = function() {console.log('吃饭')
}// 子类构造函数
function Student(){};// 核心:使子类原型指向父类实例,建立继承关系
Student.prototype = new Person()
// 修复constructor指向,避免继承混乱
Student.prototype.constructor = Student
// 子类拓展自有方法
Student.prototype.study = function(){console.log('学习')
}// 子类实例
let xm = new Student()
xm.eat() // '吃饭' 继承了父类的共有方法
console.log(xm.species) // '人类' 继承了父类的自由属性
sm.study() // '子类' 子类的扩展方法
继承中的属性与方法扩展规则
- 共有方法扩展:通过 子类构造函数.prototype.方法名 添加,所有子类实例可以共享。
- 自由属性扩展:通过 实例.属性名 添加,通过这种方式添加的属性仅当前实例拥有
- 注:扩展时避免直接覆盖原型对象,例如:Student.prototype = { … }。这样做会导致constructor等原生属性丢失,需要手动修复。
// 错误:直接覆盖原型,丢失constructor
Student.prototype = { study: function() {} };
console.log(Student.prototype.constructor); // 指向Object(错误)// 正确:逐个添加或修复constructor
Student.prototype = {constructor: Student, // 手动绑定constructorstudy: function() {}
};
三、原型链
原型链的概念
- 原型链的定义:对象的隐式原型__proto__指向创建它的构造函数的显式原型prototype,而显式原型本身也是对象,其__proto__也指向更上层的原型,这种查找形成的链式结构即为原型链。
- 原型链的核心特征:
- 查找机制:访问属性时,沿__proto__链逐级向上查找,直到找到目标属性,或者到达原型链的终点null;如果到达终点,则返回undefined。
- 关键节点:
原型链的完整结构(以Student实例为例)
// 父类构造函数
function Person(){this.species = '人类'
}function Student(){};
Student.prototype = new Person()
Student.prototype.constructor = Student
Student.prototype.study = function(){console.log('学习')
}// 实例 → Student.prototype → Person.prototype → Object.prototype → null
let xm = new Student();// 验证原型链关系
console.log(xm.__proto__ === Student.prototype); // true
console.log(Student.prototype.__proto__ === Person.prototype); // true(若Student继承Person)
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true(终点)
注:如果存在多层继承,原型链会相应延长。
原型链的查找流程
实例访问属性prop时,原型链的查找流程如下:
- 检查 实例自身 是否有prop自由属性,如果有则返回;
- 如果没有,则通过 实例.proto 查找构造函数的prototype,如果有则返回;
- 如果没有,则通过构造 函数.prototype.proto 查找父级原型;重复此过程;
- 直到 Object.prototype ;如果仍未找到,则返回 undefined
四、函数与对象的原型链特殊性
函数作为“特殊对象”的原型链
在JavaScript中,函数也是对象,因此同时拥有:
- prototype:显示原型,以供实例继承;
- proto:隐式原型,指向创建它的构造函数的原型;
函数的原型链核心规则如下:
- 所有函数(包括构造函数)都是由Function构造函数创建的,因此函数的__proto__均指向Function.prototype
- Function是“函数的函数”,其__proto__指向自身的prototype(形成闭环:Function.proto** **=== Function.prototype)
- 实例:
function Foo() {}
// 函数的__proto__指向Function.prototype
console.log(Foo.__proto__ === Function.prototype); // true
// Function自引用
console.log(Function.__proto__ === Function.prototype); // true
Object 原型链:所有对象的最终源头
<font style="color:rgb(0, 0, 0);">Object</font>
是所有对象的 “根构造函数”,其<font style="color:rgb(0, 0, 0);">prototype</font>
是原型链的顶层节点(除<font style="color:rgb(0, 0, 0);">null</font>
外)。- 所有对象(包括函数对象)的原型链最终都会指向
<font style="color:rgb(0, 0, 0);">Object.prototype</font>
,而<font style="color:rgb(0, 0, 0);">Object.prototype.__proto__ = null</font>
(原型链的终点)。
关键关系:
// 普通对象的原型链
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true// 函数对象的原型链(函数→Function.prototype→Object.prototype→null)
console.log(Function.prototype.__proto__ === Object.prototype); // true
原型链核心关系图
实例对象(xm)↓ __proto__
构造函数.prototype(Student.prototype)↓ __proto__
父级构造函数.prototype(Person.prototype)↓ __proto__
Object.prototype↓ __proto__
null(终点)// 函数对象的原型链
函数(Foo)↓ __proto__
Function.prototype↓ __proto__
Object.prototype↓ __proto__
null(终点)
五、原型链的实际应用与风险
1. 典型应用场景
- 扩展内置对象方法:给
<font style="color:rgba(0, 0, 0, 0.85) !important;">Array.prototype</font>
添加自定义方法,使所有数组可用(需谨慎,避免污染)。
// 给数组添加求和方法
Array.prototype.sum = function() {return this.reduce((a, b) => a + b, 0);
};
[1, 2, 3].sum(); // 6(所有数组均可调用)
- 实现代码复用:将工具方法挂载到构造函数原型,供所有实例共享(如表单验证方法挂载到
<font style="color:rgba(0, 0, 0, 0.85) !important;">Form.prototype</font>
)。 - polyfill 实现:为低版本浏览器补充 ES6 + 方法(如
<font style="color:rgba(0, 0, 0, 0.85) !important;">Array.prototype.includes</font>
),本质是修改内置对象的原型。
2. 风险与注意事项
- 原型污染:直接修改
<font style="color:rgba(0, 0, 0, 0.85) !important;">Object.prototype</font>
会影响所有对象(包括函数、数组等),可能导致命名冲突或逻辑异常。
// 危险:污染所有对象
Object.prototype.id = 1;
console.log([].id); // 1(数组也会继承id,不符合预期)
- 性能影响:过长的原型链会增加属性查找时间(浏览器优化会缓解,但仍需避免不必要的层级)。
- constructor 指向混乱:覆盖原型时若不修复
<font style="color:rgba(0, 0, 0, 0.85) !important;">constructor</font>
,可能导致<font style="color:rgba(0, 0, 0, 0.85) !important;">instanceof</font>
判断异常或类型识别错误。
3. 原型链相关 API
API | 作用 | 示例 |
---|---|---|
<font style="color:rgb(0, 0, 0);">Object.getPrototypeOf(obj)</font> | 获取对象的隐式原型(推荐,替代<font style="color:rgb(0, 0, 0);">__proto__</font> ) | <font style="color:rgb(0, 0, 0);">Object.getPrototypeOf(xm) === Student.prototype</font> |
<font style="color:rgb(0, 0, 0);">Object.setPrototypeOf(obj, proto)</font> | 修改对象的隐式原型(谨慎使用,影响性能) | <font style="color:rgb(0, 0, 0);">Object.setPrototypeOf(xm, Person.prototype)</font> |
<font style="color:rgb(0, 0, 0);">obj instanceof Constructor</font> | 判断实例是否属于某构造函数(检查原型链) | <font style="color:rgb(0, 0, 0);">xm instanceof Student</font> → <font style="color:rgb(0, 0, 0);">true</font> |
六、ES6 class 与原型链的关系
ES6 的<font style="color:rgba(0, 0, 0, 0.85) !important;">class</font>
语法是原型链的 “语法糖”,其底层仍依赖原型链实现继承,但代码更直观:
// 父类
class Person {constructor() {this.species = "人类";}eat() { console.log("吃饭"); } // 等同于Person.prototype.eat
}// 子类继承
class Student extends Person {constructor() {super(); // 对应父类构造函数调用}study() { console.log("学习"); } // 等同于Student.prototype.study
}// 本质与ES5原型链继承一致
const xm = new Student();
console.log(xm.__proto__ === Student.prototype); // true
console.log(Student.prototype.__proto__ === Person.prototype); // true