ECMAScript-Object

229 阅读24分钟

Object

三类对象和两类属性操作区分

  • 内置对象: 由ECMAScript规范定义的对象或类. 例如数组, 函数, 日期, 正则等等都是内置对象
  • 宿主对象: 由JavaScript解析器所嵌入的宿主对象(web浏览器定义), 就是一些DOM对象
  • 自定义对象: 由开发者创建的对象
  • 自有属性: 直接在对象上定义属性
  • 继承属性: 对象的原型对象中定义的属性

Object.defineProperty

Object.defineProperty(属性所在的对象, 属性的名字, 一个描述符对象(就是下面四个数据属性))

ECMAScript有两种属性: 数据属性与访问器属性

数据属性

  • [[Configurable]]: 配置, 是否可以delete删除属性, 是否可以修改属性, 是否可以把属性修改为访问器属性,默认false
  • [[Enumerable]]: 表示是否可以通过for-in循环返回属性,默认false
  • [[ Writable]]: 表示能否修改属性的值,默认false
  • [[Value]]: 包含这个属性的值, 读取属性值的时候,从这个位置读取, 写入属性值的时候, 把新值保存在这个位置.这个特性的默认值为undefined
var person = {
    name: "Nicholas"
}

这里创建一个名为name的属性,为它指定的值是Nicholas,也就是说,[[value]]特性将被设置为Nicholas,而对这个值的任何修改都将反映在这个位置

var person = {}
Object.definePeoperty(person, 'name', {
    // configurable: false, // 不允许配置
    writable: false, // 禁止修改
    value: 'Nicholas'
})
console.log(person.value) // Nicholas
person.name = 'Greg'
cosnole.log(person.value) // Nicholas

这里尝试创建一个name属性, 它的值是只读的, 这个值不可以被修改,如果强行修改,在非严格模式下会忽略,严格模式下报错, 如果前面配置是false,后面再写一次Object.definePeoperty, 把writable: true就会报错; configurable: false也是一样,

访问器属性

访问器属性不包含数值,他们包含一对gettersetter函数,在读取访问属性时会调用getter,这个函数负责返回有效的值; 在写入属性时会调用setter函数, 并传入新值, 这个函数负责决定如何处理数据, 访问器4个属性如下:

  • [[Configurable]]: (配置)表示能否delete删除属性, 能否修改属性特性, 能否把属性修改为数据属性, 默认值为true
  • [[Enumerable]]: 表示能否通过for-in循环返回属性
  • [[Get]]: 读取属性时调用, 默认值undefined
  • [[Set]]: 在写入属性时调用的函数, 默认值为undefiend
var book = {
    _year: 2004,
    edition: 1
}
Object.defineProperty(book, 'year', {
    get() { // 如果year不指定set那么就不能写, 如果写会忽略
        return this._year
    },
    set(newValue) {
        this._year = newValue
        this.edittion += newVlaue - 2004
    }
})
book.year = 2005
consoel.log(book.edition) // 2

创建了一个book对象, 并定义了两个默认的属性: _yearedition,_year和访问器属性year不同,访问器year上面有gettersetter函数, getter函数返回_year值,setter通过计算确定

定义多个属性: Object.defineProperties(要操作的对象, 要给操作的对象添加的属性)

// 定义多个属性
var book = {}
Object.defineProperties('book', {
    _year: {
        // 可以修改
        writable: true,
        value: 2004
    },
    edition: {
        writable: true,
        value: 1
    },
    yaer: {
        get() {/*上面有*/},
        set() {/*上面有*/}
    }
})

创建对象

对象直接量

创建对象可以使用对象直接量、关键字new方法和Object.create()函数来创建

// 直接量创建
var obj = {
    name: 'zhangsan',
    age: 18,
    say: function() {
        console.log('1111')
    }
}

工厂模式

工厂模式是设计模式中的一种, 考虑到ECMAScript无法创建类(ES3,5没有class), 开发着就发明了一种函数, 用函数来封装 以特定的接口来创建对象的细节(也就是在函数里面创建对象, 然后把对象当成接口返回出去)

function animal(name, age) {
	var o = new Object()
	o.name = name
	o.age = age
	o.say = function(){
		console.log('11111')
	}
	return o
}
var a1 = animal('zhangsan', 18)

工厂模式, 就是一个可以批量制造某种类型的东西, 就是封装方法减少重复工作, 主要是为了解决需要创建大量有重叠属性的对象, 如果每一个都new一下,然后逐一添加属性或方法, 这是一件很难受的事情, 上面的animal方法,可以批量制造出动物,这样每次只需要简单的一行代码就可以搞定一个动物的创建

缺点: 每次新建的时候都需要在内部创建一个对象,然后进行一系列操作,最后返回, 也就是创建10次, 就需要创建10次全新的对象,然后返回并赋值, 这样创建10个对象再代码间是没有问题, 但是10个都是动物, 但是不知道是哪里的动物, 就是需要判断是不是动物类型, 然而并没有方法, 只知道他是Object, 所以就出来了构造函数

构造函数没有创建对象和return, 并且都用this添加属性, 声明对象的时候通过new关键字, 这样的好处就是新建对象间是有关系的

构造函数模式

new关键字用来创建类的实例对象. 也是设计模式中的构造函数模式, 实例化对象后,也就继承类的属性和方法

function Person(name, age) {
    this.name = name
    this.age = age
    this.say = function() {
        console.log('111')
    }
}
var person = new Person('zhangsan', 18)
console.log(person.name, person.age, person.say())

上面的new做了这4件事, 如下

  • 创建一个空对象, 作为要返回的对象
  • 将构造函数的作用域(原型)赋值给新对象(this就指向这个新对象)
  • 执行构造函数中的代码(为这个对象添加属性)
  • 返回新对象

代码中的var person = new Person(), 在js引擎中person对象的__proto__ 指向Person.prototype,其实这时候new操作符做了三件事

var obj = {}
obj.__proto__ = Person.prototype
Person.call(obj)// this指向obj

实现

function Person(name, age) {
    this.name = name
    this.age = age
    this.say = function() {
        console.log('111')
    }
}
function _new (fun, ...res) { // 这个也是高阶函数, 参数是函数, 返回对象
    let obj = {}
    obj.__proto__ = fun.prototype
    fun.call(obj, ...res)
    return obj
}
let p1 = _new(Person, 'zhangsan', 18)
let p2 = new Person('lisi', 20)
console.log(p1.constructor === Person) // true
console.log(p2.constructor === Person)// true

上面p1,p2虽然是同一个函数, 但是p1, p2分别保存着Person的不同的实例, 这两个对象都有constructor(构造函数)属性, 该属性指向Person, 对象的constructor属性最初用来标识对象类型的,最后提到检测对象类型还是instanceof操作符更靠谱

构造函数的缺点: 每个方法都要在每个实例上创建一遍.

解析: 在上面p1, p2都有一个名为say的方法, 但是那两个方法不在同一个Function实例中, 也就是say是Person的私有属性, 每次创建都是不同的, 可以用p1.say === p2.say, 其实创建两个完成相同功能的函数是完全没必要的, 也就是代码冗余, 况且有this对象在, 根本不用再执行代码器前就把函数绑定到特定对象上面, 于是就有(我把上面的那个_new去掉, 功能是一样的)

function Person(name, age) {
    this.name = name
    this.age = age
    this.say = say
}
function say() {
    console.log(this.name)
}
let p1 = new Person('zhangsan', 18)
let p2 = new Person('lisi', 20)

将构造函数的方法提取出来成全局函数, 这样say包含的是一个指向函数的指针, 因此对象p1, p2就共享全局作用域中定义的同一个say函数, 这样解决了两个函数做同一件事情. 缺点: 全局作用域中的函数只能被某个对象调用; 如果对象需要定义很多方法,那就需要定义很多的全局函数, 于是就没有什么封装性, 于是就引入,设计模式之原型模式

原型(__proto__)

每一个JavaScript(对象除null外)都和原型(对象)关联, 每一个对象都是从原型继承属性

  • 所有对象直接量创建的对象都具有同一个原型对象(Object.prototype)
  • 可以通过Object.prototype获取原型的引用(地址)
  • 通过new和构造函数调用创建的对象的原型 就是 构造函数prototype属性的值(p1.__proto__ === Person.prototype)
  • new Array()、new Date()、new RegExp()与直接量创建的对象{}, 都是继承Object.prototype

注意: 直接量创建的对象{}, 的原型是Object.prototype,而new Array()创建的对象原型是Array.prototypenew Date()的创建对象原型是Date.prototypenew RegExp()创建的对象原型是RegExp.prototype

Object.prototype没有原型对象, 它不继承任何属性, 其他对象都是普通对象, 普通对象都具有原型, 所有的内置构造函数(以及大部分自定义构造函数)都是继承自Object.prototype,例如由new Date()创建的Date对象的属性同时继承自Date.prototype和Object.prototype, 这一系列链接的原型对象就是**"原型链"**(利用原型让引用类型继承另一个引用类型的属性和方法)

构造函数、原型与实例的关系: 每个构造函数都有一个原型对象, 原型对象包含一个指向构造函数的指针(constructor), 而实例都包含一个指向原型对象的内部指针(__proto__)

var o = {} // 原型对象是Object.prototype
var obj = new Object() // 创建一个空对象(注意里面继承Object.prototype)
obj.constructor === Object
var arr = new Array() // 创建一个空数组(Array.Prototype --> Object.Prototype)
var date = new Date() // 创建表示当前时间(Date.Prototype --> Object.Prototype)
var regexp = new RegExp() // 创建一个可以进行模式匹配RegExp对象(RegExp.Prototype --> Object.Prototype)
// 自定义构造函数
function Person() {}
var person = new Person() // (Person --> Function.prototype --> Objec.prototype)

Object.create()

ECMAScript定义一个名为Object.create()的静态方法(不是提供给某个对象调用的方法), 创建对象一个新对象, 其中第一个参数是这个对象的原型,第二个参数与Object.defineProperties()的第二个参数格式相同: 每个属性都是通过自己的描述符定义

var o = Object.create({name: 'zhansgan', age: 18}) // o继承name, age属性

var obj = Object.create(null) // 里面什么都没有, 输出就是个{}

var obj = Object.create(Object.prototype) // {} === Object.prototype === new Object()

// 创建一个create对象, 返回继承p属性的新对象
function create(p){
    if(p == null) throw TypeError
    if(Object.create) { // 是否兼容
        return Object.create(p)
    }
    // 不支持, 就退化
   	if(typeof !== "obj" && typeof !== "function") throw typeError
    function f() {}
    f.prototype = p
    return new f()
}
function Person(o){
    function F () {}
    // o对象把prototype给覆盖了
    F.prototype = o
    var c = new F()
    return c
}
var p = {
    name: 'zhangsan'
    language: ['C', 'C++', 'C#']
}
var a = Object.create(Person, {
    name: {value: 'lisi'}
})
console.log(a.name) // lisi

原型模式

每个函数都有prototype(原型属性), prototype就是通过调用构造函数而创建的那个对象实例的原型对象, 好处是可以让所有对象的实例共享它所包含的属性和方法

// 构造函数空函数
function Person(){} 
Person.prototype.name = 'zhangsan'
Person.prototype.name = 29
Person.prototype.say = function(){console.log('11111')}
// 调用构造函数来创造对象, 新对象具有相同的属性和方法(这些实例和方法是共享的, 在Person.prototype上)
var p1 = new Person() 
var p2 = new Person()
p1.say()
p2.say()
console.log(p1.say === p2.say) // true

只要创建一个新的函数,就会默认为函数创建一个prototype属性, 指向函数原型对象, 也就是说函数的原型保存在prototype里面, 访问Person.prototype

所有的原型对象会自动获取一个constructor(构造函数属性), 这个属性指向函数prototype属性所在的函数指针(这个constructor保存的就是Person函数里面所有属性, 导致p1.prototype.constructor === Person) 与 Person.prototype.constructor === Person

调用构造函数创建一个实例(p1, p2)之后, 该实例内部包含一个指针__proto__, 指向构造函数的原型对象p1.__proto__ === Person.prototype, 这个连接存在实例与构造函数的原型对象之间(p1与Person.prototype), 而不是存在实例与构造函数直接按

从而一个空函数的原型Person.prototype里面只有两个属性:

  • constructor 这里面保存的是当前函数的所有属性(也可以说内存中指向当前的函数)
  • __proto__: 指向原型链继承Function.prototype --> Object.prototype

azqd3R.png

isPrototypeOf() 测试实例是否有指向构造函数的prototype

Object.getPrototypeOf() 返回实例的构造函数原型, ECMAScript5 添加属性

hasOwnProperty() 检测一个属性是存在实例中,还是原型中(这个方法继承Object), 属性存在对象的实例中才会返回true

propertyIsEnumerable() 是hasOwnProperty增强版, 只检测自有属性而且能枚举的才返回true

in 操作符 通过对象能够访问给定属性时返回true, 无论属性在实例还是原型

console.log(Person.prototype.isPrototyOf(p1)) // true
console.log(Person.prototype.isPrototyOf(p2))// true
console.log(Object.getPrototypeOf(p1) === Person.prototype) // true
console.log(Object.getPrototypeOf(p2).name) // 'zhansgan'

当代码读取某个对象的某个属性时, 都会执行一次搜索,目标是具有给定名字的属性. 搜索首先从对象实例本身开始(p1). 如果在实例中找到了具有给定名字的属性, 则返回给属性; 如果没有找到则继续搜素指针指向的原型对象(Person.prototype), 在原型对象中查找具有给定名字的属性. 如果在原型对象中找到了这个属性, 则返回给属性值, 也就是说我们在调用p1.say()的时候,会先后执行两次搜素. 首先解析器问: '实例p1有sayname属性吗', 回应: '没有', 然后继续解析原型链上的, 到了Person.prototype上, 问: 'p1的原型有sayName属性吗?', 回应: '有'; 于是他就读取那个保存在原型对象中的函数. 当我们调用p2.say()的时候,将会重现相同的过程,得到相同的结果, 这就是对象实例共享原型所保存的属性和方法的基本原理

function Person() {}
Person.prototype.name = 'zhangsan'
var p1 = new Person()
var p2 = new Person()
p1.name = 'lisi'
console.log(p1.name) // lisi  实例
console.log(p1.__proto__.name) // zhangsan  原型
console.log(p2.name) // zhangsan  原型

console.log(p1.hasOwnProperty('name')) // true  在实例中
console.log(hasPrototypeProperty(p1, 'name'))// false  自定义函数
console.log('name' in p1)  // true 

delete p1.name  // 删除实例中的属性

cosnole.log(p1.hasOwnPropety('name')) // false  在原型中, 实例中的已经删除
console.log(hasPrototypeProperty(p1, 'name'))// true  自定义函数
console.log('name' in p1)  // true 

如果实例中有属性和原型中的属性相同, 则实例中的属性屏蔽原型中的属性, 除非p1.__proto__.name

// 通过in与hasOwnProperty写出hasPrototypeProperty自定义函数
// hasPrototypeProperty()   访问属性在原型中返回true, 在实例中返回false(包括被实例属性屏蔽的原型属性)
function hasPrototypeProperty(obj, name) {
    // 属性在原型中返回true  && 属性在原型与实例中返回true
    return !Object.hasOwnProperty(name) && (name in object)
}

遍历原型

Object.keys(obj) 返回一个包含所有可枚举属性的字符串数组(constructor属性不能枚举所有没有)

Object.getOwnPropertyName() 无论该对象是否可以枚举, 都可以使用该方法, 返回的是数组(结果包含不能枚取的constructor属性)

function Person() {}
Person.prototype.name = 'zhangsan'
Person.prototype.age = 18
Person.prototype.say = function() {conso le.log('1111')}
// 输出: ['name', 'age', 'say']
console.log(Object.keys(Person.prototype))
// 输出: ['constructor', 'name', 'age', 'say']
console.log(Object.getOwnPropertyNames(Person.prototype))
var p1 = new Person()
p1.name = 'Rob'
p1.age = 20
// ['name', 'age']
console.log(Object.keys(p1))
// ['name', 'age']
console.log(Object.getOwnPropertyNames(p1))

工厂原型语法

function Person() {}
console.dir(Person)
// 这里的原型链是: Person.prototype -> Function.prototype  -> Object.portotype
Person.prototype.name = 'zhangsan'
// 这个实际是重写了prototype的原型: Person.prototype-> Object.prototype
// 这种形式相当于把对象字面量形式直接赋值给Person.prototype, 然而对象字面量直接继承Object.prototype
// 那么constructor就不是指向Person构造函数,而是Object构造函数
Person.prototype = { 
    // 如果constructor真的很重要可以在这添加: constructor: Person , 不然访问的就是Object上的constructor
    age: 18
}
var p = new Person()
// p是Person的实例
console.log(p instanceof Person) // true
// Person.prototype的原型是Object.prototype
console.log(p instanceof Object) // true
// 本质上重写了prototype对象, 因此constructor属性也就变成新对象的constructor属性(指向Object)
console.log(p.constructor === Person) // false
console.log(p.constructor === Object) // true

如果添加了constructor属性, 前面说constructor不能枚取, 可以通过Object.getOwnPropertyName这种方法枚举constructor,但是上面给prototype属性手动添加constructor默认是可以枚举的, 也就是[[Enumberable]]等于true,默认为false, 这时候可以使用Object.defineProperty, 重新设置这个属性

Object.definePeoperty(Person.prototype, 'constructor', {
    enumerable: false, 
    value: Person
})

原型的动态性

function Person() {}
var friend = new Person() // 如果把new放在下面就不会报错
Person.prototype = {
    constructor: Person,
    name: 'zhansgan',
    age: 18,
    say: function(){console.log('1111')}
}
// 实例中的指针仅指向原型, 而不是构造函数
friend.say()  // 爆错

分析: 先执行第一步函数, 函数默认会创建prototype对象与constructor属性, 第二行代码new 创建一个实例p, 实例p.__proto__指向Person.prototype(这里的prototype里面没有属性) 然后Person.prototype被赋值给值类型对象(也就是Person.prototype的指针指向这个值类型), 于是最后访问: p.say()报错的原因是, p.__proto__还是指向原来的空Prototype, 并不是指向现在的prototype

dpyba6.png

原型模式缺点, 首先它省略了为构造函数传递初始化参数这一环节, 结果所有的实例在默认情况下都将取得相同属性值; 最大的问题还是其所共享的本性, 原型中所有的属性都是被共享的, 对于方法而言这是非常好的, 属性值勉强可以, 但是如果在实例上添加与原型上相同的属性,那么原型上就会屏蔽, 然而对于包含引用值的类型来说这是最大问题

function Person() {}
Person.prototype = {
    name: 'zhansgan',
    age: 18,
    arr: ['C', 'C++', 'C#']
}
var p = new Person()
var p1 = new Person()
console.log(p.name, p.arr) // zhangsan ["C", "C++", "C#"]
p.name = 'lisi'
p.arr.push('Java')
console.log(p1.name, p1.arr) // zhangsan ["C", "C++", "C#", "Java"]
console.log(p1.arr === p.arr) // true

上面由于arr数组存在于Person.prototype,而非p中, 所以修改也会通过p1.arr也会反应出来(与p.arr指向同一数组)

构造函数和原型模式组合

构造函数定义实例属性,而原型模式用于定义方法和共享属性. 结果每个实例都有自己的一份实例属性副本(new出来的都不一样), 同时又共享着对方的引用, 最大程度的节省内存

function Person(name, age) { // 构造函数模式
    this.name = name
    this.age = age
}
Person.prototype = { // 原型模式
    constructor: Person,
    sayName: function(){console.log(this.name)}
}
var p1 = new Person('zhangsan', 18)
var p2 = new Person('lisi', 20)
console.log(p1.name === p2.name) // false
console.log(p1.say === p2.say) // false

动态原型模式

把所有的信息都封装在构造函数中,而通过在构造函数中初始化原型(仅必要的情况下), 又保持同时使用构造函数和原型的缺点

function Person(name, age){
    this.name = name
    this.age = age
    if(typeof this.say !== 'function') // 动态初始化原型
    this.prototype.say = function(){console.log(this.name)}
}
var p = new Person('zhangsan', 18)
p.say()

注意上面的判断语句: 这段代码只是在初次调用构造函数的时候会调用, 此后原型已经完成了初始化,不需要做什么修改

寄生构造函数模式

思想: 创建一个函数, 该函数的作用仅仅是封装创建对象的代码, 然后在返回新创建的对象; 这种模式可以在特殊情况为对象创建构造函数

function Person (name, age) {
    var o = new Object()
    o.name = name
    o.age = age
    o.say = function() { console.log(this.name) }
    // 返回的对象o, 与构造函数与构造函数原型属性之间没有关系, 不能用instanceof来确定对象类型
    return o
}
// 这里如果写成 var p1 = Person('zhangsan', 18)就是工厂函数
var p1 = new Person('zhangsan', 18)
p1.say() // 'zhangsan'

稳妥构造函数模式

思想: 没有公共属性, 方法也不引用this的对象, 最适合在安全环境中使用(这些环境会禁止this与new)或者防止数据被其他应用程序改动时使用

稳妥构造函数与寄生构造函数类似,但是稳妥构造函数新创建的对象实例方法不引用this, 不使用new操作符调用构造函数

function person(name, age) {
    // 创建需要返回的对象
    var o = new Object()
    o.name = name
    o.age = age
    o.say = function() {console.log(name)}
    return o
}
var p = Person('zhangsan', 18)

注意: 以上这种模式创建的对象中, 除了使用say方法外, 没有别的方式访问name属性了; p中保存的是一个稳定对象,而除了调用say方法外,没有别的方式可以访问其数据成员,即使有其他代码会给这个对象添加方法和数据成员,但也不能有其他方法访问传入到构造函数中的原始数据

继承

原型链

原型链基本思想: 利用原型让一个引用类型继承另一个引用类型的属性和方法

上面Object.create()里面也解释了原型链, 这里再贴一张图, 说明原型链

azqd3R.png

首先需要明白构造函数与普通函数的对象

function Person(){}
Person.prototype.name = 'name'
console.dir(Person)
var p = new Person()
console.dir(p)
9

所以来看下面的代码

function SuperType(){
    this.property = true
}
function SubType() {
    this.subproperty = false
}
SuperType.prototype.getSuperValue = function() {
    return this.property
}
// 继承SuperType
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function() {
    return this.subproperty
}
var instance = new SubType()
console.log(instance.getSuperValue()) // true
dipEk9.png

缺点: 共享属性(与原型模式一个问题); 创建子类的实例的时候, 不能向父类传递参数(没有办法不影响所有对象实例的情况下,传递参数 )

借用构造函数

又名经典继承, 伪造对象, 思想: 在子类型构造函数内部调用超类型(父类型)构造函数

function SuperType() {  // 构造函数借用这个函数的属性
    // this指向SubType
    this.colors = ["red", "blue", "green"]
}
function SubType() {
    // 继承SuperType, this指向SubType然后传递
    SuperType.call(this)
}
var instancel = new SubType()
console.log(instancel)
dikwn0.png

在每个新建的环境下调用SuperType构造函数, 就会在新SubType对象上执行Supertype()函数中定义的所有对象初始化代码, 结果,SubType的每个实例就都会具有自己的colors属性副本

相对于原型链而言,借用构造函数有一个很大的优势, 既可以在子类型构造函数中向超类型(父类型)构造函数传递参数

function SuperType(name) {
    this.name = name
}
// 这个函数对象和上面一样
function SubType() {
    // 继承SuperType, this指向SubType然后传递
    SuperType.call(this, 'zhangsan')
    this.age = 29
}
console.dir(SubType)
var instancel = new SubType()
// instancel实例对象: age: 29, name: 'zhangsan'

缺点: 方法都在构造函数中定义, 因此函数就复用不了, 而且在父类型中定义方法, 在子类型中是不可见的

组合继承

又叫"伪经典继承": 将原型链和构造函数结合在一起, 原型链实现对原型属性和方法的继承, 借用构造函数来实现对实例属性的继承

原理: 子函数实例化, 子函数里面调用父函数, 然后把父函数的构造函数赋值给子函数的prototype(继承父函数属性和方法), 现在子函数的prototype的constructor(构造函数)指向父函数(抛弃父函数的指针, 指向子函数(于是父函数就是不可达函数)), 这时候把指向父函数的构造函数指到子函数构造函数

function SuperType(name) {
    // 这里的this是SubType.prototype
    this.name = name2
    this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function(){
    //
    console.log(this.name)
}
function SubType(name, age) {
    // 继承属性
    SuperType.call(this, name) // 第二次调用父类
    // 这里this就是SubType的实例
    this.age = age
}
// 继承方法
/*
          注意: 这里的new SuperType()会把SuperType里面的this改成SubType.prototype
            于是name, colors就在SubType.prototype里面
        */
SubType.prototype = new SuperType() // 第一次调用父类
/*
         注意: SubType.prototype里面是SuperType的prototype, 所以这个时候SubType里面的constructor指向SuperType()
        然后把SubType构造函数把constructor覆盖, 于是重新指向SubType构造函数
        */
SubType.prototype.constructor = SubType
SubType.prototype.sayAge = function(){
    console.log(this.age)
}
console.dir(SubType)
var instancel = new SubType('zhangsan', 18)
instancel.colors.push('black')
// ["red", "blue", "green", "black"]
console.log(instancel) // zhangsan
instancel.sayName()
instancel.sayAge()  // 18
var instance2 = new SubType('lisi', 20)
// ["red", "blue", "green", "black"]
console.log(instance2) 
instance2.sayName() // lisi
instance2.sayAge() // 20
12.png

在这个例子中, SuperType构造函数定义两个属性name和colors,SuperType的原型定义了一个方法sayName(),SubType构造函数在调用SuperType构造函数时,传入name参数, 接着定义自己的属性age, 然后将SuperType的实例赋值给SubType的原型, 然后在该原型上定义自己的方法, 这样就可以让两个不同的SubType实例分别拥有自己属性,包括colors属性, 又可以使用相同方法

缺点: 组合继承最大的问题就是无论什么情况下, 都会调用两次超类构造函数, 一次在创建子类原型的时候, 另一次是在子类构造函数内部

原型式继承

借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型

原型式继承必须有一个对象可以作为另一个对象的基础, 如果有这么个对象就可以把它传递给object()函数, 然后根据Person()传入其中的对象执行一次的浅复制, 所有的操作都是在F构造函数上操作

function Person(o){
    function F () {}
    // o对象把prototype给覆盖了
    F.prototype = o
    return new F()
}
var p = {
    name: 'zhangsan',
    language: ['C', 'C++', 'C#']
}
// 这里的a接收的是F构造函数
var a = Person(p)
// 这样添加是给F函数添加私有属性, 并没有修改prototype里的name
// 这个name把prototype里面的name给屏蔽了
a.name = 'lisi'
// 这种的是直接寻找内存地址, 在内存地址里面修改
a.language.push('Java')
var b = Person(p)
b.name = 'wangwu'
b.language.push('Python')
dixLsU.png

寄生式继承

寄生式继承是原型式继承紧密相关的思路

创建一个仅有封装过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回

 function object(o){
     function F () {}
     // o对象把prototype给覆盖了
     F.prototype = o
     return new F()
 }
// Person封装过程的函数
function Person(obj) {
    // 这一步也就是所谓的增强对象: 把对象添加到F函数的原型里面, 然后把F构造函数返回
    // 操作object函数的面的F, clone接收的就是F的构造函数: var clone = new F()
    var clone = object(obj)
    // 自定义属性, 私有属性
    clone.sayHi = function(){
        console.log('hi')
    }
    return clone
}
// 共享属性
var p = {
    name: 'zhangsan',
    language: ['C', 'C++', 'C#']
}
var a = Person(p)
// 这种的是直接寻找内存地址, 在内存地址里面修改
a.language.push('Java')
var a1 = Person(p)
dF9flq.png

寄生组合式继承

在组合继承上面继续扩展, 组合继承的缺点: 组合继承最大的问题就是无论什么情况下, 都会调用两次超类构造函数, 一次在创建子类原型的时候, 另一次是在子类构造函数内部。而寄生组合继承,即通过借用构造函数来使用, 通过原型链的混成形式来继承来继承方法。原理: 不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的就是超类型的一个副本而已, 本质上就是寄生继承来继承超类的原型,然后将结果指定给子类型的原型

function Persson(subType, superType){
    var prototype = object(subType.prototype)// 创建对象: 创建超类型副本
    prototype.constructor = subType // 增强对象: 为副本添加constructor, 弥补因重写原型而失去默认的constructor属性
    subType.prototype = prototype // 指定对象: 将新创建的对象(即副本)赋值给子类型原型
}
function object(o){
    function F () {}
    // o对象把prototype给覆盖了
    F.prototype = o
    console.log(o)
    return new F()
}
function Person(subType, superType){
    // 创建对象: 创建超类型副本, 
    // var prototype = new F()  prototype.__proto__ == F
    var prototype = object(superType.prototype)
    // 增强对象: 为副本添加constructor, 弥补因重写原型而失去默认的constructor属性
    // F.constructor = SuperType  修改成 SubType 
    // 所以F.prototype.constructor = SubType
    prototype.constructor = subType 
    // 指定对象: 将新创建的对象(即副本)赋值给子类型原型
    // subType.prototype.constructor = SubType
    subType.prototype = prototype 
    console.dir(subType)
}
function SubType(name, age) {
    // 这里的this指向instancel
    SuperType.call(this, name)
    this.age = age
}
function SuperType(name) {
    this.name = name
    this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function() {
    console.log(this.name)
}
Person(SubType, SuperType)
SubType.prototype.sayAge = function() {
    console.log(this.age)
}
var instancel = new SubType('zhangsan', 18)
console.log(SubType.prototype.__proto__ === SuperType.prototype)
console.log(SubType.prototype.constructor === SubType)
instancel.colors.push('black')
var instance2 = new SubType('lisi', 20)
dATcIx.png

这个例子的搞笑了体现在它调用了一次SuperType构造函数,并且避免了在SubType.prototype上面创建不必要的多余属性

总结

  • 原型链

以原型链的方式来实现继承(超类实例赋值给子类原型), 缺点: 在包含有引用类型的数据时, 会被所有的实例对象所共享,容易造成修改混乱. 还有就是在创建子类型的时候不能向超类型传递参数

  • 借用构造函数

通过在子类型的函数中调用超类型的构造函数来实现SuperType.call(this), 在子类型里面调用, this指向子类

  • 组合继承

将原型链和借用构造函数组合使用的一种方式. 通过构造函数的方式来实现属性继承SubType.prototype = new SuperType(), 通过子类型的原型设置为超类型的实例来实现方法的继承SuperType.call(this, name)

  • 原型式继承

基于已有的对象来创建新的对象, 实现原理是,向函数中传入一个对象,然后返回一个以这个对象为原型的对象var a = Person(p), 这个Person(p)就是函数内部返回的实例

  • 寄生继承

创建一个用于封装继承过程的函数, 通过传入一个对象那个,然后复制一个对象的副本,然后对象进行扩展, 最后返回这个独享。这个扩展的过程就可以理解是一种继承。这个种继承的优点就是对一个简单对象实现继承,如果这个对象不是我们的自定义类型时。缺点就是没有办法实现函数复用

  • 寄生组合继承

缺点: 就是使用超类型的实例作为子类型的原型, 导致添加了不必要的原型属性。寄生式组合组合继承的方式是使用超类型的原型的副本来作为子类型的原型, 这样就避免了创建不必要的属性