进阶篇:prototype 对象和继承

JavaScript 常被描述为一种基于原型的语言——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链,它解释了为何一个对象会拥有定义在其他对象中的属性和方法。

在传统的面向对象编程(Object Oriented Programming,OOP)语言中,首先定义类,实例按类定义持有自己的字段,方法通常仍挂在类上。JavaScript 并不把方法复制到每个实例上,而是在实例与构造函数的 prototype 之间建立一个链接(实例的内部槽 [[Prototype]],可通过 __proto__ 访问,指向构造函数的 prototype 对象),之后通过上溯原型链找到这些属性和方法。

function Person() { }
Person.prototype.hi = 'Hello,'
Person.prototype.greeting = function () {
    console.log(`${this.hi} ${this.name}!`)
}

const person = new Person();
person.name = 'lizhao';
person.greeting() // Hello, lizhao!

console.log(Person.prototype) // {hi: 'Hello,', greeting: ƒ, constructor: ƒ}
console.log(Person.prototype === person.__proto__) // true

注意: ES6 引入的 class 关键字只是语法糖,JavaScript 仍然是基于原型的。

构造器(构造函数)

传统的面向对象编程语言(C++、Java...)都有的概念。所谓就是对象的模板,对象就是类的实例。但是,JavaScript 语言的对象体系,不是基于类的,而是基于构造函数原型链

在 JavaScript 中,构造函数是对象的模板,描述实例对象的基本结构,用于生成多个具有相同结构的实例对象。本质上说,构造函数就是一个普通的函数,当使用 new 操作符调用该函数时,就可以称为构造器(构造函数)。

构造函数有以下特征:

  • 用 new 操作符来调用。
  • 函数体的 this 指向生成的对象实例。
function Person(name) {
    this.hi = 'Hello,'
    this.name = name
    this.greeting = function () {
        console.log(`${this.hi} ${this.name}!`)
    }
}
const person = new Person('lizhao');
person.greeting() // 'Hello, lizhao!'

构造函数的缺点: 同一个构造函数创建的多个实例之间,无法共享属性和方法。这样就会导致两个问题:一是系统资源的浪费,如上代码,每个实例都会创建 hi、name 属性和 greeting 方法,而 hi 属性和 greeting 方法在每个实例中都是一样的;二是构造函数与实例之间缺少联接,比如,想要统一更新所有已生成实例的 hi 属性。

new 操作符

new 操作符用于创建一个对象类型或者构造函数的实例。

new 操作符进行如下操作:

  • 创建一个空的新对象(即 {});
  • 把新对象的内部槽 [[Prototype]] 指向构造函数的 prototype(若 prototype 不是对象,则指向 Object.prototype);
  • 将新对象作为 this 的上下文;
  • 若构造函数返回对象(含函数、数组等),则返回该对象;否则返回 this。原始值返回值会被忽略。

函数内部可以使用 new.target。如果当前函数是用 new 调用的,new.target 指向当前函数(或 class 里实际被 new 的那个类),否则为 undefined

function Person() {
    console.log(new.target)
}
new Person(); // ƒ Person() { console.log(new.target) }
Person()      // undefined

原型

JavaScript 继承机制的设计思想就是,原型对象的所有属性和方法,都能被实例对象共享。这样不仅节省了内存,还建立了实例和对象之间的联系。

function Person(name) {
    this.name = name
}
Person.prototype.hi = 'Hello,'
Person.prototype.greeting = function () {
    console.log(`${this.hi} ${this.name}!`)
}

const person1 = new Person('li');
person1.greeting() // Hello, li!

const person2 = new Person('zhao');
person2.greeting() // Hello, zhao!

原型对象的属性不是实例对象自身的属性:

person1.hasOwnProperty('name')     // true
person1.hasOwnProperty('hi')       // false
person1.hasOwnProperty('greeting') // false

修改原型对象会立刻体现到所有实例对象上:

Person.prototype.hi = '你好,'
person1.greeting() // 你好, li!
person2.greeting() // 你好, zhao!

实例对象可以重写原型对象中的属性和方法。当访问一个对象的属性和方法时,JavaScript 引擎先会查找自身是否有该属性或方法,如果没有,再去原型对象查找。

person2.hi = 'Goodbye,'
person2.hasOwnProperty('hi') // true
person2.greeting() // Goodbye, zhao!

person1.greeting() // 你好, li!
  • 对象的原型 [[Prototype]](俗称隐式原型):每个普通 JS 对象内部都有一个内部插槽 [[Prototype]],它的值就是这个实例所要继承的那个对象。通过obj.__proto__(访问器属性,非规范内置名)、Object.getPrototypeOf(obj)(标准 API)获取。Object.create(null) 创建的对象,[[Prototype]] = null,没有继承原型。
  • 函数的 .prototype 属性(构造函数显式原型)prototype 只是函数身上的一个普通属性,只有函数对象默认自带这个属性(普通对象、数组实例、普通字面量对象默认没有 .prototype 这个属性)。当这个函数被当作构造函数 new Fn() 调用时:new 出来的实例的 [[Prototype]] = 构造函数的 Fn.prototype

简单理解:[[Prototype]]是实例身上继承指向谁(所有普通对象都具备这个内部插槽);.prototype是构造函数身上的属性,用来给未来 new 出来的实例当原型(只有函数默认拥有)。

用 new 创建的实例,其原型通常就是该构造函数的 prototype 对象。用 Object.create (proto)Object.setPrototypeOf 时,原型可以是任意对象,不必经过构造函数。

constructor 属性

构造函数的 prototype 对象有一个 constructor 属性,默认指向该构造函数。

所有实例对象(Object.create(null) 创建的对象除外)都将从其构造函数的原型中继承了一个 constructor 属性,默认情况下,该属性指向实例对象的构造函数。

注意: 实例的 constructor 属性是继承而来的,不是自身属性。当获取实例的 constructor 时,其实是从实例的原型,也就是其构造函数的 prototype 对象中读取。

function Person() { }
const person = new Person();

Person.prototype.constructor === Person // true
person.constructor === Person.prototype.constructor // true
person.constructor === Person // true

之所以构造函数要在 prototype 对象有这样一个指向自身的属性,就是为了给实例对象继承,让实例对象知道自己是哪一个构造函数创建的。

constructor 属性应用: 一是获取实例对象的构造函数的名称,二是用于从一个实例对象创建另一个属于同一构造函数的实例对象。

person.constructor.name // 'Person'

const person2 = new person.constructor()
person2.constructor === person.constructor // true

可以为任何数据类型(null、undefined 除外)指定 constructor 属性,但基本类型(String、Number、Boolean 等)不会保留更改,也不会抛出异常(基本类型设置其他属性也是如此)。也就是,每次将基本类型当成对象使用时,都是根据原始值实时创建实例对象,其他属性被忽略了。

function Person() { }
const str = 'abc'
str.constructor = Person
str.prop = 123

console.log(str.constructor) // ƒ String() { [native code] }
console.log(str.prop)        // undefined

基本上,除了基本类型外,任何对象都可以更改 constructor 属性的值。

const arr = []
arr.constructor = Person
console.log(arr.constructor) // ƒ Person() { }

改变构造函数 prototype 对象或者实例对象的 constructor 属性不会影响 instanceof 运算符。

arr instanceof Array // true

注意: 如果构造函数的 prototype 对象修改为其他构造函数的 prototype 对象,建议修改其 constructor 属性,也就是重新将 constructor 属性指向构造函数。

function Person() {
    this.hi = 'Hello'
}
function Student() { }
Person.prototype = Student.prototype

const person = new Person()
console.log(person.constructor) // ƒ Student() { }
console.log(person.hi)          // Hello

如上,person 实例本来是 Person 构造函数创建的,但是,由于 Person.prototype 对象修改为 Student.prototype 对象,导致 person 实例的 constructor 属性继承的是 Student.prototype.constructor,指向的是 Student 构造函数。

这样可能会引发一些问题,比如,当我们依赖实例对象的 constructor 属性创建新实例时:

const person2 = new person.constructor()
console.log(person2.hi) // undefined. 不是 Person 创建的实例

如上代码,本意是创建另一个 Person 实例,而实际创建的是一个 Student 实例。这是由于 person.constructor 指向的并不是真正创建 person 实例的构造函数。

因此,为了避免不必要的问题,不要让两个构造函数共用同一个 prototype 对象,推荐:

Person.prototype = Object.create(Student.prototype)
Person.prototype.constructor = Person

若写成 Person.prototype = Student.prototype,再改 constructor 会同时改掉 Student.prototype.constructor

instanceof 运算符

instanceof 运算符返回一个布尔值,用于判断一个实例对象的原型链上是否包含某个构造函数的 prototype 属性。

[] instanceof Array  // true
[] instanceof Object // true

[].__proto__ === Array.prototype // true
Array.prototype.__proto__ === Object.prototype // true
// 数组实例的原型是Array.prototype -> Array.prototype 的原型是 Object.prototype。所以,数组实例的原型链上也包含 Object.prototype。

需要注意的是: person instanceof Person 返回 true,并不意味着永远返回 true,因为 Person.prototype 属性的值是可变的,同样,person 的原型链也是可变的。

Person.prototype = { a: 1 }
person instanceof Person // false
Object.setPrototypeOf(person, { b: 1 })
person instanceof Person // false

注意: 在多个全局对象的情况下(如:多个 frame 或多个窗口),不同的全局环境拥有不同的全局对象,从而拥有不同的内置类型构造函数。这可能会引发一些问题。比如,[] instanceof window.frames[0].Array 会返回 false,因为 window.Array.prototype !== window.frames[0].Array.prototype

注意: JavaScript 中绝大多数对象都是 Object 的实例,anyObject instanceof Object 通常为 true,但也有例外:一是原始值(不是对象),二是用 Object.create(null) 创建的对象。

const str1 = 'abc'
const str2 = new String('abc')
str1 instanceof Object // false
str2 instanceof Object // true

const obj = Object.create(null)
typeof obj    // 'object'
obj.__proto__ // undefined
obj instanceof Object // false

其他相关的属性和方法

Object.create()

ECMAScript 5 中引入了 Object.create(),用来创建一个指定原型的新对象

Object.create(null) // 创建一个原型为null的实例对象

function Constructor(){}
Object.create(Constructor.prototype, {
    prop1: { // 数据属性
        writable: true,
        configurable: true,
        value: "hello"
    },
    prop2: { // 访问器属性
        configurable: false,
        get: function () { return 10 },
        set: function (value) {}
    }
});

Object.create() 的主要用于从一个实例对象生成另一个实例对象,这是推荐的做法,比依赖实例对象的 constructor 属性更安全:

function Person() { }
const person = new Person()
Object.setPrototypeOf(person, null)

new person.constructor() // Uncaught TypeError: person.constructor is not a constructor
Object.create(Object.getPrototypeOf(person)) // {}

getPrototypeOf()

返回指定对象的原型(内部[[Prototype]]属性的值)。这是获取对象的原型的标准方法,与对象的 __proto__ 属性返回的是同一原型对象。

const arr = [];
Object.getPrototypeOf(arr) === arr.__proto__ // true

setPrototypeOf()

为对象设置一个指定的原型 (一个对象或 null),即修改对象 [[Prototype]] 属性的值。

const arr = [];
Object.setPrototypeOf(arr, String.prototype)
// arr 会失去 Array.prototype 上的属性和方法了,但会继承 String.prototype 上的属性和方法。

arr.push(1)     // Uncaught TypeError: arr.push is not a function
arr.charAt(0)   // 继承了 String.prototype.charAt,但 this 仍是数组

isPrototypeOf()

返回布尔值,判断对象是否在另一个对象的原型链上。作用与 instanceof 类似,但调用方式相反:A.prototype.isPrototypeOf(b) 对应 b instanceof Ainstanceof 还会考虑 Symbol.hasInstance

Object.prototype.isPrototypeOf([]) // true
[] instanceof Object // true

__proto__

返回实例对象内部 [[Prototype]] 属性,也就是构造函数的 prototype 对象。

注意: __proto__ 属性是一个访问器属性(一个 getter 和一个 setter 函数),是可读写的

注意: __proto__ 长期由浏览器实现,ES6 才写入规范附录 B(遗留特性),不是首选 API。读取原型用 Object.getPrototypeOf()(ES5),设置用 Object.setPrototypeOf()(ES6)。__proto__Object.create(null) 的对象上不存在。

原型链

在 JavaScript 中,每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链,它解释了为何一个对象会拥有定义在其他对象中的属性和方法。

所有对象(Object.create(null) 创建的对象除外)的原型最终都可以上溯到 Object.prototype,也就是说,所有对象都继承了 Object.prototype 的属性,如常见的 valueOf()、toString() 方法。

那么,Object.prototype 对象有没有原型呢?

有,Object.prototype 的原型是 null。

Object.getPrototypeOf(Object.prototype) // null

null 没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是 null。

读取对象的某个属性时,JavaScript 引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的 Object.prototype 还是找不到,则返回 undefined。

如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做 “覆盖”。

注意: 在原型链上查找属性会沿链逐级访问,层次越深越慢;找不到的属性会走到链的尽头。这与 for...in 枚举可枚举属性不是同一件事:普通属性读取不区分是否可枚举。

注意: 改变原型对象的属性,在每一个 JavaScript 引擎和浏览器中都是一个非常慢且影响性能的操作,原型对象的属性修改会影响到所有继承自该原型的实例对象。一般建议是,创建一个新的且可以继承该原型的对象(推荐使用 Object.create()),再重写对象的属性和方法。

继承

继承是面向对象编程中的一个概念。继承可以使得子类具有父类的各种属性和方法,而不需要再次编写相同的代码。在子类继承父类的同时,还可以重新定义某些属性,并重写某些方法,即覆盖父类的原有属性和方法,使其获得与父类不同的功能。另外,为子类追加新的属性和方法也是常见的做法。

一个简单示例

JavaScript 的继承基于原型链。常见写法是:子构造函数里 call 父构造函数初始化实例字段,再用 Object.create(Parent.prototype) 接上原型,避免两个构造函数共用同一个 prototype 对象。

function Person(name) {
    this.hi = `Hello, I'm ${name}!`
}
Person.prototype.greet = function () {
    console.log(this.hi)
}
function Student(name, grade) {
    Person.call(this, name)
    this.grade = grade
}
Student.prototype = Object.create(Person.prototype)
// Object.create(),是为防止 Student.prototype 的改动影响 Person.prototype
Student.prototype.constructor = Student
Student.prototype.read = function () { }

const student = new Student("Lizhao", 100)
console.log(student) // Student {hi: "Hello, I'm Lizhao!", grade: 100}

多重继承

JavaScript 不提供多重继承功能,但可以通过扩展构造函数的 prototype 对象来实现多重继承。

function Manager() { }
Manager.prototype.manage = function () { }
Object.assign(Student.prototype, Manager.prototype)

class 关键字

ES6 引入了一套新的关键字来定义类,包括 class、constructor、static、extends 和 super。

注意: ES6 中的类仍然是基于原型的,class 关键字只是语法糖。

class Person {
    constructor(name) {
        this.hi = `Hello, I'm ${name}!`
    }
    greet() {
        console.log(this.hi)
    }
}

class Student extends Person {
    constructor(name, grade) {
        super(name);
        this.grade = grade
    }
    read() { }
}
const student = new Student("Lizhao", 100)
console.log(student) // Student {hi: "Hello, I'm Lizhao!", grade: 100}

constructor 方法是一个特殊的方法,用于创建和初始化一个由 class 创建的对象。

super 用来引用父类:在 constructor 里必须先调用 super() 才能使用 this;也可以用 super.method() 调用父类原型上的方法。父类赋在 this 上的实例属性,在 super() 之后通过 this 访问,而不是 super.属性名

static 关键字用来定义一个类的静态属性和静态方法,静态属性和静态方法不会出现在类实例中。

extends 关键字用于创建一个类作为另一个类的一个子类。

常见问题

prototype、__proto__、getPrototypeOf?

prototype 对象大概是 JavaScript 中最容易混淆的名称之一。

prototype 的翻译是 “原型”。所以,你可能认为,prototype 对象指向当前对象的原型对象,其实不是。

一个对象的 prototype 对象的属性和方法,是用于被其他对象(子对象、实例)继承的,而一个对象的原型对象定义的是该对象可以从其他对象(父对象、构造函数)承继而来的属性和方法。

遵循 ECMAScript 标准,每个对象都有一个内部槽 [[Prototype]],指向它的原型。用 new 创建时,这个原型通常是构造函数的 prototype 对象;也可以是 Object.create 传入的对象。

读取原型的标准方法是 Object.getPrototypeOf()(ES5)。__proto__ 是引擎先实现、后来写入规范附录 B 的遗留访问器,与 getPrototypeOf 读到的是同一条链,但不推荐新代码使用。

为什么要修改 A.prototype.constructor = A ?

function Person() { }
function Student() { }
Student.prototype = Person.prototype
Student.prototype.constructor = Student
const student = new Student()

在原型链继承中,经常会看到类似 Student.prototype.constructor = Student 的代码。

为什么要这样修改呢?不修改会有什么后果呢?

事实上,就算不修改,new Student() 也不会有什么问题。

这一行代码的作用仅仅是,让实例对象能够找到它真正的构造函数

比如,从一个实例对象生成另一个实例对象时,如果没有这行代码,会出现如下情况:

function Person() {
    this.hi = 'Hello'
}
function Student() { }
Person.prototype = Student.prototype

const person = new Person()
console.log(person.hi) // 'Hello'。是Person的实例,有属性 hi。

const person2 = new person.constructor()
console.log(person2.hi) // undefined。不是Person的实例,所以没有属性 hi。

参考资料

MDN - 对象原型

MDN - new 运算符

阮一峰 - prototype 对象

© lizhao all right reserved,powered by Gitbook文件修订时间: 2026-08-31 21:57:16

results matching ""

    No results matching ""