1、构造函数
用new
关键字来调用的函数,称为构造函数。用于创建某一类对象(实例化),其首字母大写,用来区分普通函数
// 字面量创建对象为new Object创建对象的语法糖
let obj = {
name: '名称',
age: 18
}
let obj = new Object({
name: '名称',
age: 18
})
构造函数中的属性和方法称之为成员
实例成员: 通过构造函数内部的this
添加的属性,只能通过实例化对象来访问
静态成员: 在构造函数本身上添加的属性,只能通过构造函数来访问(函数也是一个对象)
function Person(name, age) {
// 实例成员
this.name = name
this.age = age
}
//静态成员
Person.sex = '男'
let personObj = new Person('张三', 18) // 实例化
console.log(personObj) // {name: "张三", age: 18}
console.log(personObj.sex) // undefined 实例无法访问静态成员sex属性
console.log(Person.age) // undefined 通过构造函数无法直接访问实例成员
console.log(Person.sex) // '男' 通过构造函数可直接访问静态成员
2、原型
2.1 概念
每一个对象从被创建开始就和另一个对象关联,从另一个对象上继承其属性,另一个对象就是原型(原型对象)
2.2 作用
存储对象共享的属性,来为其它对象提供共享属性
2.3 类型
显式原型(prototype): prototype
是函数才具有的属性,这个属性指向一个对象
隐式原型(__ proto__或[[Prototype]]): __proto__是对象的一个属性,默认值是构造函数的 prototype 属性值(即原型对象)
// 构造函数的 prototype(显式原型)和其实例的__proto__(隐式原型)是指向同一个地方(原型对象)
function Person(name, age) {
this.name = name
this.age = age
}
let personObj = new Person('张三', 18)
console.log(Person.prototype) // 显式原型,函数的 prototype 属性
console.log(personObj.__proto__) // 隐式原型,对象的 __proto__ 属性
console.log(Person.prototype === personObj.__proto__) // true
2.4 属性
原型对象中默认有一个constructor
属性指向构造函数,一旦替换了原型对象,这个constructor
属性就需要手动赋值
3、原型链
3.1 概念
实例对象和原型对象通过// proto/*/*属性层层关联,直到内置对象 Object 的原型对象(null)止,从而形成原型链
3.2 作用
让实例化对象可以通过原型链找到公用的属性或方法(通过原型链实现属性的继承)
3.3 检测
通过instanceof
运算符检测构造函数的prototype
属性是否出现在某个实例对象的原型链上,即A instanceof B
,判断B
的prototype
是否在A
的原型链上
// 函数也是对象,所以也有__proto__属性,函数的构造函数是 Function
function Person(name, age) {
this.name = name
this.age = age
}
console.log(Person instanceof Function) // true, Person.__proto__ === Function.prototype
console.log(Person instanceof Object) // true,Function.prototype.__proto__ === Object.prototype
instanceof
模拟实现
function myInstanceof(obj, Constructor) {
let proto = Object.getPrototypeOf(obj) // 获取对象的原型,即 obj.__proto__
let prototype = Constructor.prototype // 获取构造函数的 prototype 对象
// 判断构造函数的 prototype 对象是否在对象的原型链上
while (true) {
if (!proto) return false // 到达原型链顶层还未找到则返回 false
if (proto === prototype) return true // 对象实例的隐式原型等于构造函数显示原型则返回 true
proto = Object.getPrototypeOf(proto)
}
}
3.4 属性查找
当访问一个对象的属性时,先在对象的本身找,找不到就沿着原型链查找,直到找到为止。如查找到最顶层的原型对象中也没有找到,就结束查找,返回undefined
4、继承
继承就是子类可以使用父类的所有功能,并且对这些功能进行扩展
4.1 原型链继承
将父类的实例作为子类的原型
function Parent(name, age) {
this.name = name
this.age = age
this.hobby = ['read', 'ball']
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child(name) {
this.name = name
}
// 通过手动修改 Child 构造函数的原型对象指向 Parent 构造函数的实列,Child.prototype = {name: '张三', age: 18}
Child.prototype = new Parent('张三', 18)
let childObj = new Child('李四')
console.info(childObj) // {name: '李四'}
childObj.getName() // '李四',childObj 本身没有 getName 函数,沿着原型链在 Parent 原型中找到 getName 函数执行,this 指向 childObj 实例
console.log(childObj.age) // 18,childObj 本身没有 age 属性,沿着原型链在 Child 原型中找到 age 函数
let childObj2 = new Child('王五')
childObj.hobby.push('walk')
console.log(childObj2.hobby) // ['read', 'ball', 'walk'],原型对象的所有属性被所有实例共享
缺点:
-
来自原型对象的所有属性被所有实例共享,所以如果修改了原型对象的引用类型数据,所有子类的数据会同步
-
子类型实例不能给父类型构造函数传参,只能在父类实例化时传值,而不能直接在子类实例化传值给父类
4.2 借用构造函数继承
通过call/apply
复制父类的实例属性给子类,即在子类构造函数内部使用call/apply
来调用父类构造函数
function Parent(name, age) {
this.name = name
this.age = age
this.getAge = function () {
console.log(this.age)
}
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child(name, age, sex) {
Parent.call(this, name, age) // 执行 Parent 父类构造函数, 复制父类的实例属性给子类
this.name = '李四' // 覆盖实例对象 name 属性
this.sex = sex // 实例对象新增 sex 属性
}
// Child 实例化时既可以传参给 Parent 父类型函数也可以传参给 Child 子类型函数
let childObj = new Child('张三', 18, '男')
console.log(childObj instanceof Parent) // false childObj 实例并不是父类的实例,只是子类的实例
console.log(childObj instanceof Child) // true
childObj.getAge() // 18,每次创建一个实例都会生成一个 getAge 函数
childObj.getName() // childObj.getName is not a function,Child 和 Parent 原型没有关联,childObj 实例在原型链中无法找到 getName 函数
缺点:
-
子类不能继承父类原型上的属性和方法
-
实例并不是父类的实例,只是子类的实例
-
无法实现函数复用,每个子类都有父类实例函数的副本,影响性能,即便是相同的函数方法,也会同样的复制一份,而不会共享同一个方法
4.3 组合继承
用原型链实现对原型属性和方法的继承,用借用构造函数技术来实现实例属性的继承
function Parent(name, age) {
this.name = name
this.age = age
this.hobby = ['read', 'ball']
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child(name, age, sex) {
// 第二次调用 Parent 构造函数, 复制父类的实例属性给子类,子类实例中的属性和方法会覆盖子类原型(父类实例)上的属性和方法
Parent.call(this, name, age)
this.sex = sex // 实例对象新增 sex 属性
}
Child.prototype = new Parent() // 第一次调用 Parent 构造函数
Child.prototype.constructor = Child // Child.prototype 重新赋值,原有的 constructor 属性需要手动添加
let childObj = new Child('张三', 18, '男') // Child 实例化时既可以传参给 Parent 父类型函数也可以传参给 Child 子类型函数
console.log(childObj instanceof Parent) // true
console.log(childObj instanceof Child) // true
console.log(childObj.sex) // '男',即可以继承父类实例属性和方法,也能够继承父类原型属性和方法
childObj.getName() // '张三'
let childObj2 = new Child('王五', 20, '男')
childObj.hobby.push('walk') // 引用属性不被所有实例共享
console.log(childObj2.hobby) // ['read', 'ball']
缺点:
-
父类构造函数会被调用两次(原型链继承和构造函数继承)
-
生成了两个实例,子类实例中的属性和方法会覆盖子类原型(父类实例)上的属性和方法,增加了不必要的内存
4.4 原型式继承
利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型,本质是对象浅拷贝(不用实例化父类了,直接实例化一个临时副本实现了相同的原型链继承)
// objcetCreate 函数是 Object.create 的模拟实现
function objcetCreate(obj) {
function F() {}
F.prototype = obj
F.prototype.constructor = F
return new F()
}
let person = {
name: '张三',
age: 18,
hobby: ['read', 'ball']
}
let personObj1 = objcetCreate(person) // 没有创建构造函数的情况下,实现了原型链继承
let personObj2 = objcetCreate(person)
personObj1.hobby.push('walk')
console.log(personObj2.hobby) // ['read', 'ball', 'walk']
缺点:
- 对象的所有属性被所有实例共享
4.5 寄生式继承
在原型式继承的基础上再封装一层,来增强对象,之后将这个对象返回
let person = {
name: '张三',
age: 18,
hobby: ['read', 'ball']
}
function createAdd(obj) {
let clone = Object.create(obj)
// 增强对象的属性方法
clone.addMethod = function () {
console.log(obj.name) // '张三'
}
return clone
}
let personObj = createAdd(person)
personObj.addMethod()
缺点:
-
对象的所有属性被所有实例共享
-
无法实现函数复用,每次创建对象都会重新创建一遍方法
4.6 寄生组合式继承
原型链继承修改成通过Object.create
来手动指定原型对象
// 在组合继承需要修改的地方
Child.prototype = new Parent() // 组合继承存在父类构造函数调用 2 次,修改成使用 Object.create 指定原型对象,其它不变
Child.prototype = Object.create(Parent.prototype)
4.7 混入方式继承
一个子类继承多个父类,在寄生组合继承的基础上使用Object.assign
来合并不同父类的原型对象
// 在寄生组合式继承上需要修改的地方
function Child(name, age, sex) {
Parent.call(this, name, age) // 原来的父类
OtherParent.call(this, sex) // 新增的父类
}
Child.prototype = Object.create(Parent.prototype)
Object.assign(Child.prototype, OtherParent.prototype) // 新增的父类原型对象
4.8 class 中的继承
类似寄生组合继承的语法糖,主要使用extends
关键字实现继承
class Parent {
constructor(name, age) {
this.name = name
this.age = age
this.hobby = ['read', 'ball']
}
// 类的所有方法都定义在类的 prototype 属性上面
getName() {
console.log(this.name)
}
}
class Child extends Parent {
constructor(name, age, sex) {
super(name, age) // 调用父类的 constructor(name, age)
this.sex = sex
}
}
let childObj = new Child('张三', 18, '男')
console.log(childObj instanceof Parent) // true
console.log(childObj instanceof Child) // true
console.log(childObj.sex) // '男'
childObj.getName() // '张三'
let childObj2 = new Child('王五', 20, '男')
childObj.hobby.push('walk') // 引用属性不被所有实例共享
console.log(childObj2.hobby) // ['read', 'ball']