原型相关知识
一、创建对象
1.1 工厂模式
这种模式抽象了创建具体对象的过程。用函数来封装以特定接口创建对象的细节,返回创建后的对象
function createPerson(name, age, job) {
var o = new Object()
o.name = name
o.age = age
o.job = job
o.sayHello = function () {
console.log(`我叫${this.name},今年${this.age},工作${this.job}`);
}
return o
}
const ming = createPerson('小明', 22, '程序员')
ming.sayHello() //我叫小明,今年22,工作程序员
const hong = createPerson('小红', 24, '程序媛')
hong.sayHello() //我叫小红,今年24,工作程序媛
- 特性:
- 解决了创建多个相似对象的问题
- 缺点:
- 没有解决对象识别的问题(即怎样知道一个对象的类型)
1.2 构造函数模式
用来创建特定类型的对象;用来初始化新创建的对象的
// 改造上方的工厂模式
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayHello = function () {
console.log(`我叫${this.name},今年${this.age},职业${this.job}`);
}
}
let person1 = new Person('ming', 12, 'student')
person1.sayHello() // 我叫ming,今年12,职业student
let person2 = new Person('hong', 13, 'student')
person2.sayHello() // 我叫hong,今年13,职业student
- 特性:
- 构造函数应该以大写字母开头,用来和普通函数作区分
- 使用 [[new]] 操作符来创建实例
- 可以将它的实例标识为一种特定的类型(优于工厂模式)
- 缺点:
- 每个方法 [[function]] 都在每个实例上重新创建了一遍。因此不同实例上的同名函数是不相等的
- 拓展:[[new]] 操作符调用构造函数创建实例的过程
- 创建一个新的对象
- 将构造函数的作用域赋给新对象(this指针指向新对象)
- 执行构造函数的代码(为新对象添加属性)
- 返回新对象
- 拓展:[[instanceof]] 操作符
- 用来判断是否是某个对象的实例
- 所有对象均继承自Object,因此 xxx instanceof Object 结果都为true
1.3 原型模式
创建的每个函数都会有一个prototype属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法;
function Person() { }
Person.prototype.name = '默认名字'
Person.prototype.age = 100
Person.prototype.job = '机器人'
Person.prototype.sayHello = function () {
console.log(`我叫${this.name},今年${this.age},职业${this.job}`);
}
const person1 = new Person()
person1.name = '小明'
person1.sayHello() // 我叫小明,今年100,职业机器人 -- name来自实例
const person2 = new Person()
person2.sayHello() // 我叫默认名字,今年100,职业机器人 -- 来自原型
-
特性:
- 所有对象实例共享它所包含的属性和方法。
- 实例与构造函数之间没有直接联系。构造函数的prototype属性 指向 原型对象;实例的[[prototype]]属性 指向 构造函数的原型对象;原型对象的constructor属性 指向 构造函数;
- 访问实例上的属性:他会先搜索实例上是否存在该属性,存在返回;不存在接着会搜索原型上是否存在该属性,同样存在返回,不存在接着搜索上一层的原型,最后找不到返回undefiend
-
缺点
- 省略了为构造函数传递初始化参数这一环节,所有的实例在默认情况下都将取得相同的属性值。
- 共享产生的问题:对于方法[[function]]很合适(解决了纯构造函数模式下的问题),对于基本属性也说的过去,对于包含引用类型值的属性存在很大问题(每个实例对引用类型属性进行修改,都会同步修改到原型对象中的属性,造成数据问题)
function Citys() { } Citys.prototype = { constructor: People, country: '中国', relationsProvinces: ['北京', '天津'], name: '山西', sayHello: function () { console.log(`我是${this.name},建交城市有:${this.relationsProvinces.join('、')}`); } } const city1 = new Citys() city1.name = '重庆' city1.relationsProvinces.push('山东') // 实例city1单独增加数据,不希望共享给实例city2。 city1.sayHello() // 我是重庆,建交城市有:北京、天津、山东 const city2 = new Citys() city2.sayHello() // 我是山西,建交城市有:北京、天津、山东,共享了实例city1的数据
-
拓展:
- isPrototypeOf() 原型对象方法:如果实例的[[prototype]]属性 指向构造函数的原型对象的话,该方法返回true
Person.prototype.isPrototypeOf(person1) // true
- Object.getPrototypeOf() ES5新增方法:这个方法接受一个[[实例对象]]参数,该方法返回实例的[[prototype]]属性
Object.getPrototypeOf(person1) == Person.prototype // true Object.getPrototypeOf(person1).name // '默认名字'
- hasOwnProperty() 继承自Object:检测属性存在于实例中还是原型中,该方法在给定属性属于对象实例中才返回true
person1.name = '小明' person1.hasOwnProperty('name') // true -- 存在实例中 delete person1.name person1.hasOwnProperty('name') // false -- 存在原型中
- [[in]] 操作符(此处讲解单独使用):通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中
person1.name = '小明' person1.hasOwnProperty('name') // true -- 存在实例中 "name" in person1 // true -- 存在实例中 delete person1.name person1.hasOwnProperty('name') // false -- 存在原型中 "name" in person1 // true -- 存在原型中 // 结合hasOwnProperty() 和 in 操作符,可以判断出给定的属性到底存在于实例中还是原型中 /** * @description: 给定属性是否存在实例中 * @param {Object} object 实例对象 * @param {String} name 给定的属性 * @return {Boolean} true => 存在于实例中,反之存在原型中 */ function hasPropertyInInstance(object, name) { if (!(name in object)) throw Error('既不存在实例中,也不存在原型中') return (name in object) && object.hasOwnProperty(name) } person1.age = 20 hasPropertyInInstance(person1, "age") // true hasPropertyInInstance(person1, "xxx") // Error: 既不存在实例中,也不存在原型中 hasPropertyInInstance(person2, "name") // false
- isPrototypeOf() 原型对象方法:如果实例的[[prototype]]属性 指向构造函数的原型对象的话,该方法返回true
1.4 组合使用构造函数模式和原型模式
构造函数模式用于定义[[实例属性]],原型模式用于定义[[方法]]和[[共享的属性]]。这样每个实例都会有自己的一份实例属性的副本,同时又共享着对方法的引用,最大限度的节省内存。
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.friends = ['李雷', '马冬梅']
}
Person.prototype = {
constructor: Person,
sayHello() {
console.log(`我叫${this.name},今年${this.age},职业${this.job}`);
}
}
const person1 = new Person('小明', 20, '程序员')
person1.friends.push('雷欧')
person1.sayHello() // 我叫小明,今年20,职业程序员
console.log(person1.friends); // ['李雷', '马冬梅', '雷欧']
const person2 = new Person('小红', 23, '程序媛')
person2.sayHello() // 我叫小红,今年23,职业程序媛
console.log(person2.friends); // ['李雷', '马冬梅']
- 特性:
- 兼顾构造函数模式和原型模式的优点
- 支持向构造函数传递参数
- 目前使用最广泛、认同度最高的一种创建自定义类型的方法。
1.5 动态原型模式
将所有信息封装在构造函数内部,通过在构造函数中初始化原型(仅在必要的情况下)。换言之:可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
if (typeof this.sayHello !== 'function') {
Person.prototype.sayHello = function () {
console.log('我是原型中的方法:' + this.name);
}
}
}
const person1 = new Person('小明', 16, '学生')
person1.sayHello = function () {
console.log('my name is xiaoming');
}
person1.sayHello() // my name is xiaoming
const person2 = new Person('小红', 20, '暂无')
person2.sayHello() // 我是原型中的方法:小红
- 特性:
- 在构造函数内部 + 原型初始化(必要情况下)
- 不能使用对象字面量重写原型,如果在已经创建了实例的情况下重写原型,会切断现有实例与新原型之间的联系y
1.6 寄生构造函数模式
创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。
// 模拟场景:假设我们想创建一个具有额外方法的特殊数组,由于不能直接修改Array构造函数,因此可用这个模式
function SpecialArray() {
// 创建数组
let arr = new Array()
// 添加值
arr.push.apply(arr, arguments)
// 添加方法
arr.toPipedString = function () {
return this.join('|')
}
return arr
}
const colors = new SpecialArray('red', 'yellow', 'blue')
console.log(colors.toPipedString()); // red|yellow|blue
console.log(colors instanceof SpecialArray); // false
console.log(SpecialArray.prototype.isPrototypeOf(colors)); // false 构造函数创建的对象 与 构造函数原型之间 没有关系
console.log(Object.getPrototypeOf(colors) == SpecialArray.prototype); // false 对象的[\[prototype]]属性 与 构造函数原型之间 没有关系
- 特性:
- 写法和工厂模式一模一样,方法名要首字母大写代表构造函数。
- 存在问题:
- 返回的对象 与 构造函数之间没有关系
- 返回的对象 与 构造函数的原型之间没有关系
- 不可以用[[instanceof]]操作符来确定对象类型
- 可以使用其他模式的情况下,不建议使用这种模式
1.7 稳妥构造函数
没有公共属性,方法也不引用this的对象,写法类似于寄生构造函数方式
function Person(name, age, job) {
// 创建要返回的对象
var o = new Object()
// 可以在次定义私有变量和函数
const type = '私有'
// 添加方法
o.sayHello = function () {
console.log('my name is ' + name);
}
return o
}
const person1 = new Person('小明', 18, '大学生')
console.log(person1.name); // undefined
person1.sayHello() // my name is 小明
- 特性:
- 与寄生构造函数模式类似,但有两点不同
-
- 新创建对象的实例方法不引用this
-
- 不适用[[new]]操作符调用构造函数
-
- 除了调用特定的方法外,没有别的方式可以访问其数据成员。即使有其他代码会给这个对象添加方法或数据成员,但也不可能有别的方法访问传入到构造函数中的原始数据。
- 与寄生构造函数模式类似,但有两点不同
- 存在问题:
- 同寄生构造函数模式一样,同样无法使用[[instanceof]]操作符来确定对象类型
二、继承
2.1 原型链继承
利用原型让一个引用类型继承另一个引用类型的属性和方法。
function SuperType() {
this.property = 'Super'
}
SuperType.prototype.getSuperProperty = function () {
console.log(this.property);
}
function SubType() {
this.subProperty = 'Sub'
}
// 继承了 SuperType
SubType.prototype = new SuperType()
SubType.prototype.getSubProperty = function () {
console.log(this.subProperty);
}
var instance1 = new SubType()
instance1.getSubProperty() // Sub
instance1.getSuperProperty() // Super 过程:搜索instance1中,再搜索SubType.prototype,再搜索Super.prototype
- 特性:
- instance 和 isPrototypeOf() 判断原型链上的关系都会返回true
- 子类型中的方法同名会覆盖超类型中的方法,谨慎定义
- 问题:
- 包含引用类型值属性的原型存在问题:引用值属性会被所有实例共享。
2.2 借用构造函数
(别名:伪造对象或经典继承):在子类型构造函数的内部调用超类型构造函数
function SuperType(name) {
this.colors = ['red', 'yellow', 'green']
this.name = name
}
function SubType() {
// 继承SuperType
SuperType.apply(this, arguments)
this.printColor = function () {
console.log(this.name, this.colors);
}
}
const instance1 = new SubType('小红')
instance1.colors.push('blue')
instance1.printColor() // 小红 ['red', 'yellow', 'green', 'blue']
const instance3 = new SubType('小兰')
instance3.printColor() // 小兰 ['red', 'yellow', 'green']
- 特性:
- 可以通过构造函数进行传参
- 问题:
- 如果是单独的借用构造函数模式,无法避免构造函数存在的问题–方法都在构造函数中定义,因此函数复用就无从说起了。
2.3 组合继承(借用构造函数+原型链)-- 推荐
使用原型链实现对原型属性和方法的继承,通过借用构造函数实现对实例属性的继承
function SuperType(name) {
this.name = name
this.colors = ['red', 'yellow', 'green']
}
function SubType(name, age) {
SuperType.call(this, name) // 第二次调用SuperType()
this.age = age
}
SubType.prototype = new SuperType() // 第一次调用SuperType()
SubType.prototype.constructor = SubType
SubType.prototype.printInfo = function () {
console.log(this.name, this.age, this.colors);
}
const instance1 = new SubType('小明', 10)
instance1.colors.push('blank')
instance1.printInfo() // 小明 10 (4) ['red', 'yellow', 'green', 'blank']
const instance2 = new SubType('小红', 15)
instance2.printInfo() // 小红 15 (3) ['red', 'yellow', 'green']
- 特性:
- 兼顾借用构造函数和原型链的优点
- 问题:
- 无论什么情况下,都会调用两次超类型构造函数:1.在创建子类型原型的时候; 2.在子类型构造函数内部。子类型最终包含超类型对象的全部实例属性
2.4 原型式继承
借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型
function object(o) {
// 创建临时构造函数
function F() { }
// 传入对象作为构造函数的原型
F.prototype = o
// 返回临时实例
return new F()
}
- 特性:
- object()对传入其中的对象执行了一次浅拷贝。
- ES5中新增了原型式继承的规范化方法:Object.create([[object]],[[描述符定义]])
- 问题:
- 引用值属性会被所有实例共享
2.5 寄生式继承(与原型式继承紧密相关)
创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回对象。
function createAnother(original) {
// 调用函数创建一个新对象
var clone = Object.create(original) // 或者var clone = object(original) // object为原型式继承中的示例方法
// 以某种方式来增强这个对象
clone.sayHi = function () {
console.log('hi');
}
// 返回这个对象
return clone
}
- 特性:
- 使用场景:主要考虑对象而不是自定义类型和构造函数的情况下适用。
- 前面示例中的object()函数不是必需的,任何能够返回新对象的函数都适用
- 问题:
- 和构造函数模式类似,函数复用率低。
2.6 寄生组合式继承
借用构造函数来继承属性,通过原型链的混成模式来继承方法。
function inheritPrototype(subType, superType) {
// 创建超类型原型副本对象
var prototype = Object.create(superType.prototype)
// 增强对象
prototype.constructor = subType
// 指定对象
subType.prototype = prototype
}
- 特性:
- 寄生组合式继承是引用类型最理想的继承方式。