JavaScript 原型

一、核心概念

1. 什么是原型

JS 是基于原型的面向对象语言,每个对象都有一个隐式属性 __proto__(原型对象),指向它的构造函数的 prototype
- prototype函数独有,函数的显式原型(原型模板)
- __proto__所有对象独有,对象的隐式原型(访问原型的入口)

2. 三大关系

// 构造函数
function Person() {}
// 实例对象
const p = new Person();
  1. 实例.proto === 构造函数.prototype
console.log(p.__proto__ === Person.prototype); // true
  1. 构造函数.prototype.constructor === 构造函数本身
console.log(Person.prototype.constructor === Person); // true
  1. 所有普通对象最终都指向 Object.prototype
    Object.prototype.__proto__ === null(原型链终点)

二、原型链

1. 定义

访问对象属性/方法时:
1. 先在自身查找
2. 找不到 → 顺着 __proto__ 往上找(原型链)
3. 直到 Object.prototype,还找不到返回 undefined

2. 完整原型链图示

p(实例) 
  ↓ __proto__
Person.prototype 
  ↓ __proto__
Object.prototype 
  ↓ __proto__
null (链结束)

示例:

Person.prototype.say = function(){ console.log('hello') }
p.say() // 自身没有,沿原型链找到 Person.prototype 上的方法

三、函数、对象、内置构造函数原型

1. FunctionObject 特殊关系

  • 所有函数都是 Function 的实例
  • Function.__proto__ === Function.prototype(函数自引用)
  • Object 也是一个函数:Object.__proto__ === Function.prototype

2. 常见内置原型

// 数组
const arr = []
arr.__proto__ === Array.prototype
Array.prototype.__proto__ === Object.prototype

// 字符串
const str = ''
str.__proto__ === String.prototype

四、原型上添加属性/方法

1. 原型方法(共享,省内存)

推荐:方法写在 prototype,所有实例共用一份

function Person(name) {
  this.name = name // 实例自有属性
}
// 挂载到原型
Person.prototype.sayName = function() {
  console.log(this.name)
}

const p1 = new Person('张三')
const p2 = new Person('李四')
p1.sayName() // 张三
p2.sayName() // 李四

2. 实例属性(独有)

this.xxx 挂载在实例自身,每个实例独立一份。

3. 属性遮蔽

实例和原型有同名属性,优先使用实例自身属性(遮蔽):

Person.prototype.age = 18
p1.age = 20 
console.log(p1.age) // 20(读自身)
delete p1.age       // 删除实例属性
console.log(p1.age) // 18(读原型)

五、原型继承(ES5 经典继承)

1. 寄生组合式继承(ES5 最优继承)

// 父类
function Parent() {
  this.parentVal = '父属性'
}
Parent.prototype.parentFn = function(){}

// 子类
function Child() {
  Parent.call(this) // 继承父类实例属性
  this.childVal = '子属性'
}

// 继承父类原型方法
Child.prototype = Object.create(Parent.prototype)
// 修正 constructor 指向
Child.prototype.constructor = Child

const c = new Child()

六、ES6 class 与原型

class语法糖,底层依然是原型:

class Person {
  constructor(name) {
    this.name = name // 实例属性
  }
  // 方法自动挂载到 Person.prototype
  say() {}
}

const p = new Person()
console.log(p.__proto__ === Person.prototype) // true

class 继承 extends

底层还是原型链继承:

class Parent {}
class Child extends Parent {}

七、常用原型 API

  1. obj.hasOwnProperty(prop)
    判断属性是否为实例自有(不查原型)
p.hasOwnProperty('name') // true
p.hasOwnProperty('sayName') // false
  1. Object.getPrototypeOf(obj)
    标准获取原型(推荐,替代 proto
Object.getPrototypeOf(p) === Person.prototype // true
  1. Object.setPrototypeOf(obj, proto)
    修改对象原型(不推荐,影响性能)

  2. isPrototypeOf()
    判断是否在对方原型链上

Person.prototype.isPrototypeOf(p) // true

八、要点总结

  1. prototype 属于函数__proto__ 属于对象
  2. 实例 .__proto__ 永远指向构造函数的 prototype
  3. 原型链终点:Object.prototype.__proto__ = null
  4. 方法放原型:共享、节省内存
  5. class 只是原型的语法糖,本质没变
  6. hasOwnProperty 区分自身属性 / 原型属性

九、易错点

  1. 不要直接赋值 Child.prototype = new Parent()(会继承多余实例属性)
  2. 修改原型会影响所有已有实例
  3. 箭头函数没有 prototype,不能当构造函数
  4. null / undefined 没有 __proto__