JavaScript 原型
构造函数
除了使用对象字面量
和new Object
创建对象外,也可以使用构造函数来创建对象
构造函数是用来创建对象的函数。构造函数命名一般以大写字母开头,通过new
关键字来调用构造函数
- 使用
new
关键字调用函数的行为被称为实例化, 新创建的实例继承了构造函数的所有属性 - 实例化构造函数时没有参数时可以省略
()
- 构造函数内部无需写
return
,返回值即为新创建的对象 - 箭头函数不能作为构造函数(箭头函数没有
this
)
function Pig(name) {
this.name = name
}
const pig = new Pig('佩奇')
console.log(pig) // Pig { name: '佩奇' }
function Dog() {
this.name = '大花'
}
const dog = new Dog
console.log(dog) // Dog { name: '大花' }
new
实例化执行过程:
创建一个新的空对象
将新对象的
__proto__
指向构造函数的原型对象构造函数
this
指向新对象执行构造函数代码
返回新对象
我们可以模拟这个过程,手写new
function mynew(Func, ...args) {
// 创建一个新对象
const obj = {}
// 新对象原型指向构造函数原型对象
obj.__proto__ = Func.prototype
// 将构建函数的this指向新对象
let result = Func.apply(obj, args)
// 根据返回值判断
return result instanceof Object ? result : obj
}
初学者很容易把构造函数和普通函数混淆,实际上new关键字也能对普通函数使用,也能创建一个对象,但为了避免混淆,规定new关键字只能对构造函数使用
与构造函数关联的属性和方法分为两类:实例成员、静态成员
- 实例成员:通过构造函数创建的对象称为实例对象,实例对象中的属性和方法称为实例成员,实例对象之间互不影响
- 静态成员:静态成员属于构造函数本身,在所有实例之间共享,并且可以通过构造函数的名称直接访问,而不需要创建构造函数的实例。静态方法中的this指向构造函数
function Person() {
this.name = '小明' // 实例成员
this.sayHi = function() { // 实例成员
console.log('大家好~')
}
}
Person.eyes = 2 // 静态成员
Person.walk = function() { // 静态成员
console.log('走路')
}
const p = new Person() // 实例对象
console.log(Person.eyes) // 访问静态成员
console.log(p.name) // 访问实例成员
原型对象
在 JavaScript 中,原型对象是实现继承的核心机制。每个函数在创建时,都会自动拥有一个名为prototype
的属性,这个属性指向一个对象,我们称之为原型对象
- 原型对象可以挂载属性和方法,对象实例化不会多次创建原型上的属性和方法,节约内存
- 构造函数通过原型分配的属性和方法是所有对象所共享的
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayHi = function () {
console.log(this.name + 'hi~')
}
Person.prototype.classNum = '101'
const zs = new Person('张三', 18)
const ls = new Person('李四', 19)
zs.sayHi() // 张三hi~
ls.sayHi() // 李四hi~
console.log(zs.classNum) // 101
console.log(ls.classNum) // 101
对象原型
实例对象都会有一个属性 __proto__
指向构造函数的原型对象,之所以实例对象可以使用构造函数的原型对象上的属性和方法,就是因为对象有 __proto__
属性的存在
注意:
__proto__
是JS非标准属性,推荐使用Object.getPrototypeOf()
来访问对象原型[[prototype]]
(ES5)和__proto__
(ES6)意义相同,用来表明当前实例对象指向哪个原型对象
function Person(name) {
this.name = name
}
Person.prototype.greet = function () {
console.log('Hello, my name is ' + this.name)
}
const alice = new Person('Alice')
// __proto__ 与 Object.getPrototypeOf()一致
console.log(Object.getPrototypeOf(alice) === alice.__proto__) // true
// 获取实例的原型
console.log(Object.getPrototypeOf(alice) === Person.prototype) // true
// 原型对象本身也是一个对象,可以有自己的原型
console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype) // true
constructor
每个原型对象里面都有个constructor
属性,该属性指向该原型对象的构造函数
构造函数.prototype.constructor === 构造函数
- 原型对象找构造函数:
原型对象(构造函数.prototype).constructor
- 实例对象找构造函数:
实例对象.__proto__.constructor
(此处__proto__
可省)
使用场景:
- 如果有多个对象的方法,我们可以给原型对象采取对象形式赋值,但是这样就会覆盖构造函数原型对象原来的内容,修改后的原型对象
constructor
不再指向当前构造函数。此时,我们可以在修改后的原型对象中,添加一个constructor
来指向原来的构造函数
function Star (name) {
this.name = name
}
Star.prototype = {
constructor: Star, //重新指回,此处不能使用this(指向window)
sing () { console.log('唱歌') },
dance () { console.log('跳舞') }
}
const pig = new Star('GGBang') //创建对象一定要在修改原型之后
pig.sing() //唱歌
构造函数、原型、实例对象三者关系图如下:

原型链
基于原型对象的继承使得不同构造函数的原型对象关联在一起,并且这种关联的关系是一种链状的结构,我们将原型对象的链状结构关系称为原型链
- JavaScript中,被继承的函数称为超类型(父类、基类),继承的函数称为子类型(子类、派生类)
Object
是 JavaScript 中所有对象的supertype
(超类,父型),也就是原型链的最顶层
上图归类为以下几点:
- 所有的构造函数,都是通过
Function
构造函数实例化完成,因此构造函数的__proto__
(函数也是对象)指向Function.prototype
,即Function
构造函数的原型对象 - 所有的普通对象,其
__proto__
指向其对应的构造函数的原型对象 Object.prototype
是所有对象的原型链的起点,是原型链的根基,再往上找对象原型则返回null
function Person(name) {
this.name = name
}
// 每个构造函数的对象原型(__proto__)指向 Function.prototype
console.log(Person.__proto__ === Function.prototype) // true
console.log(Object.__proto__ === Function.prototype) // true
console.log(Function.__proto__ === Function.prototype) // true
// 构造函数原型对象(prototype)的对象原型(__proto__)指向 Object.prototype
console.log(Function.prototype.__proto__ === Object.prototype) // true
console.log(Person.prototype.__proto__ === Object.prototype) // true
// 原型链终端的对象原型(__proto__)指向null
console.log(Object.prototype.__proto__) // null
原型链查找规则:
- 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性
- 如果没有就查找它的原型(
__proto__
指向的原型对象) - 如果还没有就查找原型对象的原型(Object的原型对象)
- 依此类推一直找到 Object 为止(null)
__proto__
对象原型的意义就在于为对象成员查找机制提供一个查找路线
instanceof
可以使用
instanceof
运算符用于检测构造函数的prototype
属性是否出现在某个实例对象的原型链上与
typeof
区别:typeof
用于判断基本数据类型instanceof
用于判断引用数据类型
// 实例对象 instanceof 构造函数
function Person (name) {
this.name = name
}
function Person1 (name) {
this.name = name
}
const zs = new Person('张三')
console.log(zs instanceof Person) // true
console.log(zs instanceof Person1) // false
// 数组
const arr = [1, 2, 3]
console.log(arr instanceof Array) // true
console.log(arr instanceof Object) // true
console.log(arr.__proto__ === Array.prototype) // true
console.log(Array.prototype.__proto__ === Object.prototype) // true
console.log(Array.__proto__ === Function.prototype) //true
原型继承
继承是面向对象编程的另一个特征,通过继承进一步提升代码封装的程度,JavaScript 中大多是借助原型对象实现继承的特性
原型链继承
原型链继承是 JavaScript 中最基本的继承方式,它通过将子类的原型指向父类的实例来实现继承,继承到的属性
// 父类
function Animal (name) {
this.name = name
}
Animal.prototype.sayName = function () {
console.log(this.name)
}
// 子类
function Dog () { }
Dog.prototype = new Animal('Default') // 原型链继承
Dog.prototype.constructor = Dog
const dog1 = new Dog()
dog1.sayName() //Default
原型链继承会有一些问题:
- 共享属性: 所有实例共享原型对象上的属性和方法。如果一个实例修改了原型上的属性,其他所有实例也会受到影响。这可能导致意外的行为,特别是在修改引用类型的属性时
- 无法向父类传递参数: 在使用原型链继承时,子类无法向父类构造函数传递参数。所有子类实例都会共享相同的父类实例,无法在创建子类实例时为父类构造函数传递参数
- 原型对象上的引用类型属性共享: 如果原型对象上有引用类型的属性,那么所有实例将共享相同的引用,可能导致修改一个实例的属性会影响其他实例
出现的问题如下:
// 父类
function Animal (name) {
this.name = name
this.play = [1, 2, 3]
}
Animal.prototype.sayName = function () {
console.log(this.name)
}
// 子类
function Dog () { }
Dog.prototype = new Animal('Default') // 原型链继承
Dog.prototype.constructor = Dog
const dog1 = new Dog()
const dog2 = new Dog()
console.log(Dog.prototype.name) // 输出 "Default"
console.log(dog1.name) // 输出 "Default"
console.log(dog2.name) // 输出 "Default"
dog1.name = 'New Dog 1' // 修改实例的属性,不影响其他实例
console.log(Dog.prototype.name) // 输出 "Default"
console.log(dog1.name) // 输出 "New Dog 1"
console.log(dog2.name) // 输出 "Default"
Dog.prototype.name = 'New Default' // 修改原型对象上的属性,影响所有实例
console.log(Dog.prototype.name) // 输出 "New Default"
// dog1实例上name属性覆盖了原型对象上的name属性
console.log(dog1.name) // 输出 "New Dog 1"
console.log(dog2.name) // 输出 "New Default"
// 使用push等方法直接修改原数组时,其他实例对象也会受到影响
console.log(dog1.play) // [1, 2, 3]
console.log(dog2.play) // [1, 2, 3]
dog1.play.push(4) // 此处修改的是原型链上的play属性,而不是在dog1对象上新增
// 如若改成 dog1.play = [1, 2, 3, 4] 则dog2不受影响
console.log(dog1.play) // [1, 2, 3, 4]
console.log(dog2.play) // [1, 2, 3, 4]
在 JavaScript 中,当实例对象访问一个属性时,它会按照以下顺序查找:
- 实例对象自身属性: 如果实例对象直接拥有该属性,就返回该属性的值
- 原型链上的属性: 如果实例对象没有直接拥有该属性,JavaScript 引擎会沿着原型链向上查找,找到第一个拥有该属性的对象,并返回该属性的值
构造函数继承
构造函数继承通过在子类构造函数中使用 call()
或 apply()
方法来调用父类构造函数,并传递子类实例作为上下文,从而实现继承
构造函数继承优化了原型链继承中:共享属性造成的问题、无法向父类传参的问题、修改原型对象造成的问题
//父类
function Animal (name) {
this.name = name
this.sayHi = function () {
console.log('Hi, I am ' + this.name)
}
}
Animal.prototype.walk = function () {
console.log(this.name + ' is walking.')
}
//子类
function Dog (name, breed) {
Animal.call(this, name) // 调用父类构造函数,继承父类属性
this.breed = breed
}
const dog = new Dog('Buddy', 'Golden Retriever')
console.log(dog.name) // 输出:Buddy
console.log(dog.breed) // 输出:Golden Retriever
dog.sayHi() // 输出:Hi, I am Buddy
dog.walk() // 报错,walk() 方法未被继承
使用构造函数继承只能继承父类的实例属性和方法,不能继承原型属性或者方法
组合继承
组合继承是将原型链继承和构造函数继承结合起来使用的方式,它通过调用父类构造函数并设置子类原型指向父类的实例来实现继承
// 父类
function Animal (name) {
this.name = name
this.sayHi = function () {
console.log('Hi, I am ' + this.name)
}
}
Animal.prototype.sayName = function () {
console.log(this.name)
}
// 子类
function Dog (name, breed) {
// 使用构造函数继承,继承实例属性
Animal.call(this, name)
this.breed = breed
}
// 使用原型链继承,继承原型上的方法
Dog.prototype = new Animal()
Dog.prototype.constructor = Dog
const dog = new Dog('Buddy', 'Golden Retriever')
// 原型的方法
dog.sayName() // 输出 "Buddy"
// 父类的方法
dog.sayHi() // 输出 "Hi, I am Buddy"
console.log(dog.breed) // 输出 "Golden Retriever"
原型链继承:本质是通过原型对象来间接继承父类的实例属性和方法(父类属性和方法全部挂载在原型对象上)
构造函数继承:直接继承父类的实例属性和方法(父类属性和方法在子类实例对象上),但不能继承原型上的属性和方法(毕竟没有任何联系)
组合继承:包含上述两种继承的优点,父类的实例属性和方法在子类实例对象上,同时也继承了原型上的属性和方法
组合继承依然有一个小缺点,就是在调用父类构造函数时会调用两次,一次是为了继承实例属性,另一次是为了继承原型上的方法。这个问题可以通过使用 ES6 的
Object.create()
或者其他方式来避免
寄生组合继承
寄生组合继承是为了解决组合继承的一些性能问题而提出的一种继承方式
组合继承的问题:在创建子类实例时,父类的构造函数会被调用两次,一次是为了创建子类实例的属性,另一次是为了设置子类原型链上的方法
寄生组合继承在组合继承的基础上进行了改进:
- 利用寄生(parasitic)方式修复子类原型: 创建一个空的中间构造函数,该构造函数的原型对象是父类的原型对象的副本。这样,就不需要调用父类构造函数来创建不必要的属性
- 设置子类的原型为中间构造函数的实例: 这一步与组合继承相同,将子类的原型设置为一个父类的实例,但这次不再是直接使用
new Parent()
,而是使用一个临时的构造函数,该构造函数的原型是Parent.prototype
的副本
Object.create()
静态方法以一个现有对象作为原型,创建一个新对象
function inheritPrototype (child, parent) {
// 此处无需再调用一次父类构造函数
child.prototype = Object.create(parent.prototype)
child.prototype.constructor = child
}
// 父类
function Animal (name) {
this.name = name
}
Animal.prototype.sayName = function () {
console.log("My name is " + this.name)
}
// 子类
function Dog (name, breed) {
// 调用父类构造函数
Animal.call(this, name)
this.breed = breed
}
// 利用寄生组合继承
inheritPrototype(Dog, Animal)
Dog.prototype.bark = function () {
console.log("Woof!")
}
var myDog = new Dog("Buddy", "Golden Retriever")
// 调用父类的方法
myDog.sayName() //My name is Buddy
// 调用子类的方法
myDog.bark() //Woof!