创建对象

44 阅读12分钟

虽然使用Object 构造函数或对象字面量可以方便地创建对象,但这些方式也有明显不足:创建具有同样接口的多个对象需要重复写很多代码。

工厂模式

工厂模式的主要工作原理是用函数来封装创建对象的细节,从而通过调用函数来达到复用的目的。

 function createPerson(name, age, job) {
     let o = new Object()
     o.name = name
     o.age = age
     o.job = job
     o.sayName = function() {
         console.log(this.name)
     }
     return o
 }
 let person1 = createPerson('Nicholas', 29, 'Software Engineer')
 let person2 = createPerson('Greg', 27, 'Doctor')

函数createPerson 接收3 个参数,根据这几个参数构建了一个包含Person 信息的对象。可以用不同的参数多次调用这个函数,每次都会返回包含3 个属性和1 个方法的对象。这种工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)。

构造函数模式

【优点】

构造函数模式相对于工厂模式来说,所创建的对象和构造函数建立起了联系,因此可以通过原型来识别对象的类型。

【缺点】

造成了不必要的函数对象的创建,因为在JS 中函数也是一个对象,因此如果对象属性中包含函数的话,那么每次都会新建一个函数对象,浪费了不必要的内存空间,因为函数是所有的实例都可以通用的。

 function Person(name, age, job) {
     this.name = name
     this.age = age
     this.job = job
     this.sayName = function() {
         console.log(this.name)
     }
 }
 let person1 = new Person('Nicholas', 29, 'Software Engineer')
 let person2 = new Person('Greg', 27, 'Doctor')
 ​
 // 1 没有显示地创建对象
 // 2 属性和方法直接赋值给了this
 // 3 没有return

【构造函数和普通函数的区别】

1、构造函数也是一个普通函数,创建方式和普通函数一样,但是构造函数习惯上首字母大写。

2、调用方式不一样,普通函数直接调用,构造函数要用关键字new 来调用。调用时,构造函数内部会创建一个新对象,就是实例,普通函数不会创建新对象。

3、构造函数内部的this 指向实例,普通函数中的this,在严格模式下指向undefined,非严格模式下指向window 对象。

4、构造函数默认的返回值是创建的对象(也就是实例),普通函数的返回值由return 语句决定

要创建Person 的实例,应使用new 操作符。以这种方式调用构造函数会执行如下操作:

1、在内存中创建一个新对象

2、这个新对象内部的[[Prototype]]特性被赋值为构造函数的prototype 属性

3、构造函数内部的this 指向新对象

4、执行构造函数内部的代码(给新对象添加属性和方法)

5、如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象

原型模式

这种方式相对于构造函数模式来说,解决了函数对象的复用问题。

使用原型对象的好处是可以预定义属性,方法。这些属性和方法会被实例对象所共享。

缺点:全部属性和方法都共享;不能初始化参数

 function Person(name) {}
 Person.prototype.name = 'lucius'
 Person.prototype.getName = function () {
     console.log(this.name)
 }
 var person = new Person()

用一个包含所有属性和方法的对象字面量来重写整个原型对象。

缺点:以对象字面量方式创建原型方法会破坏之前的原型链。相当于重写了原型链,constructor 属性不再指向构造函数。

function Person(name) {}
Person.prototype = {
    name: 'lucius',
    getName: function () {
        console.log(this.name)
    }
}
const person = new Person()

将constructor 在重写原型的时候写进去,指向构造函数。

优点:通过constructor属性可以重新访问构造函数

缺点:原型模式的缺点,即不能初始化参数,所有参数和方法都共享

function Person(name) {}
Person.prototype = {
    constructor: Person,
    name: 'alfred',
    getName: function () {
        console.log(this.name)
    }
}
const person1 = new Person()

组合模式:构造函数用于定义实例属性,原型模式用于定义方法和共享的属性。

function Person(name) {
    this.name = name
}
Person.prototype = {
    constructor: Person,
    getName: function () {
        console.log(this.name)
    }
}
const person3 = new Person()

动态原型模式:把信息封装在构造函数中,通过在构造函数中初始化原型。这一种模式将原型方法赋值的创建过程移动到了构造函数的内部,通过对属性是否存在的判断,可以实现仅在第一次调用函数时对原型对象赋值一次的效果。这一种方式很好的2对上面的混合模式进行了封装。

优点:保持了构造模式和原型模式的优点。

function Person(name) {
    this.name = name
    if (typeof this.getName !== "function") {
        Person.prototype.getName = function () {
            console.log(this.name)
        }
    }
}
const person4 = new Person()

原型

JavaScript 是基于原型的语言,但是和传统面向对象语言不同,JavaScript 通过原型来继承和实例化的时候,并不会像传统面向对象语言那样将类的行为复制到实例或者子类当中去,而是将多个对象进行使用原型链关联来实现。

每一个构造函数的内部都有一个prototype 属性,它的属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。

当使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的prototype 属性对应的值,在ES5 中这个指针被称为对象的原型。一般来说不应该能够获取到这个值的,但是现在浏览器中都实现了__proto__属性来访问这个属性。ES5 中新增了一个Object.getPrototypeOf() 方法,可以通过这个方法来获取对象的原型。

默认情况下,所有原型对象自动获得一个名为constructor 的属性,指回与之关联的构造函数

在自定义构造函数时,原型对象默认只会获得constructor 属性,其他的所有方法都继承自Object。 每次调用构造函数创建一个新实例,这个实例的内部 [[Prototype]] 指针就会被赋值为构造函数的原型对象。

构造函数通过prototype 属性链接到原型对象

实例与构造函数没有直接联系,与原型对象有直接联系,通过__proto__ 链接到原型对象,它实际上指向隐藏特性[[Prototype]]

原图链接:mollypages.org/tutorials/j…

image-20230327114303206

<来源:滴滴面试>
Object.prototype.name = 'object'
Function.prototype.name = 'function'
function Person() {}
const p = new Person()
console.log(Person.name)
console.log(p.name)
console.log(p.__proto__.__proto__.constructor.constructor.constructor.constructor.constructor)

// function
// object
// Function 构造函数 - ƒ Function()

原型层级

在通过对象访问属性时,会按照这个属性的名称开始搜索。搜索开始于对象实例本身。如果在这个实例上发现了给定的名称,则返回该名称对应的值。如果没有找到这个属性,则搜索会沿着指针进入原型对象,然后在原型对象上找到属性后,再返回对应的值。

只要给对象实例添加一个属性,这个属性就会遮蔽(shadow)原型对象上的同名属性,也就是虽然不会修改它,但会屏蔽对它的访问。即使在实例上把这个属性设置为null,也不会恢复它和原型的联系。不过使用delete 操作符可以完全删除实例上的这个属性,从而让标识符解析过程能够继续搜索原型对象。

继承

很多面向对象语言都支持两种继承:接口继承和实现继承。

前者只继承方法签名,后者继承实际的方法。接口继承在ECMAScript 中是不可能的,因为函数没有签名。实现继承是ECMAScript 唯一支持的继承方式。

ECMA-262 把原型链定义为ECMAScript 的主要继承方式。其基本思想就是通过原型继承多个引用类型的属性和方法。

原型链

当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。

相互关联的原型对象的链接被称为原型链。当我们读取实例属性或者方法的时候,会通过[[prototype]]查找即沿着原型链进行委托查找,一直找到最顶层为止。

function SuperType() {
    this.property = true
}
SuperType.prototype.getSuperValue = function() {
    return this.property
}

function SubType() {
    this.subproperty = false
}

// 继承SuperType
// 赋值重写SubType 最初的原型,替换为SuperType 的实例,这意味着SuperType 实例可以访问的所有属性和方法也会存在于SubType.prototype
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function() {
    return this.subproperty
}
const instance = new SubType()
console.log(instance.getSuperValue()) // true

SubType 没有使用默认原型,而是将其替换成了一个新的对象。这个新的对象是SuperType 的实例。这样,SubType 的实例不仅能从SuperType 的实例中继承属性和方法,而且还与SuperType 的原型挂上了钩。

于是instance (通过内部的[[Prototype]])指向SubType.prototype,而SubType.prototype(作为SuperType 的实例又通过内部的[[Prototype]])指向SuperType.prototype。

由于SubType.prototype 的constructor 属性被重写为指向SuperType,所以instance.constructor 也指向SuperType。

调用instance.getSuperValue() 经过了3步搜索:instance、SubType.prototype、SuperType.prototype,最后一步才找到这个方法。对属性和方法的搜索会一直持续到原型链的末端。

【默认原型】

默认情况下,所有引用类型都继承自Object,这也是通过原型链实现的。任何函数的默认原型都是一个Object 的实例,这意味着这个实例有一个内部指针指向Object.prototype。

【原型与继承关系】

console.log(instance of Object) // true
console.log(Object.prototype.isPrototypeOf(instance)) // true

【原型链问题】

  • 原型中包含的引用值会在所有实例间共享,这也是为什么属性通常会在构造函数中定义而不会定义在原型上的原因。
  • 子类型在实例化时不能给父类型的构造函数传参。无法在不影响所有对象实例的情况下把参数传进父类的构造函数。
盗用构造函数 - constructor stealing

为了解决原型包含引用值导致的继承问题。也称作对象伪装经典继承

思路:在子类构造函数中调用父类构造函数。

缺点:必须构造函数中定义方法,因此函数不能重用;子类不能访问父类原型上定义的方法。

function SuperType() {
    this.colors = ['red', 'blue', 'green']
}
function SubType() {
    // 继承SuperType
    SuperType.call(this)
}

const instance1 = new SubType()
instance1.colors.push('black')
console.log(instance1.colors) // ['red', 'blue', 'green', 'black']

const instance2 = new SubType()
console.log(instance2.colors) // ['red', 'blue', 'green']
组合继承 - 使用最多

组合继承/伪经典继承综合了原型链和盗用构造函数。

思路:使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。

缺点:使用了两种不同的模式,所以对于代码的封装性不够好。

function SuperType(name) {
    this.name = name
    this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function() {
    console.log(this.name)
}
function SubType(name, age) {
    // 继承属性
    SuperType.call(this, name)
    this.age = age
}

SubType.prototype = new SubType()
SubType.prototype.sayAge() = function() {
    console.log(this.age)
}
const instance1 = new SubType('Nicholas', 29)
instance1.colors.push('black')
console.log(instance1.colors) // ['red', 'blue', 'green', 'black']
instance1.sayName() // 'Nicholas'
instance1.sayAge() // 29

const instance2 = new SubType('Greg', 27)
console.log(instance2.colors) // ['red', 'blue', 'green']
instance2.sayName() // 'Greg'
instance2.sayAge() // 27
寄生式继承

类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。

缺点:通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。

function createAnother(original) {
    let clone = Object(original) // 通过调用函数创建一个新对象
    clone.sayHi = function() { // 以某种方式增强这个对象
        console.log('hi')
    }
    return clone
}

const person = {
    name: 'Nicholas',
    friends: ['shelby', 'Court', 'Van']
}

const anotherPerson = createAnother(person)
anotherPerson.sayHi() // 'hi'
寄生式组合继承 - 引用类型继承的最佳模式

组合继承其实也存在效率问题:父类构造函数始终会被调用两次:一次是在创建子类原型时调用,另一次是在子类构造函数中调用。

// 组合继承问题
function SuperType(name) {
    this.name = name
    this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function() {
    console.log(this.name)
}
function SubType(name, age) {
    SuperType.call(this, name) // 第二次调用SuperType()
    this.age = age
}

SubType.prototype = new SuperType() // 第一次调用SuperType()
SubType.prototype.constructor = SubType
SubType.prototype.sayAge = function() {
    console.log(this.age)
}

寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。

思路:不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。即使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。

function inheritPrototype(subType, superType) {
    const prototype = Object(superType.prototype) // 创建对象
    prototype.constructor = subType // 增强对象
    subType.prototype = prototype // 赋值对象
}

function SuperType(name) {
    this.name = name
    this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function() {
    console.log(this.name)
}
function SubType(name, age) {
    SuperType.call(this, name)
    this.age = age
}
inheritPrototype(subType, superType)
SubType.prototype.sayAge = function() {
    console.log(this.age)
}

这里只调用了一次SuperType 构造函数,避免了SubType.prototype 上不必要也用不到的属性。而且原型链保持不变。

寄生构造函数模式

创建一个构造函数,封装创建对象的代码,然后返回新创建的对象。

返回的对象与构造函数或者构造函数的原型之间没有关系,不能用instanceof 来确定对象类型。

和工厂模式相比,就是多使用了一个new,实际上两者的结果是一样的。

它主要是基于一个已有的类型,在实例化时对实例化的对象进行扩展。这样既不用修改原来的构造函数,也达到了扩展对象的目的。

【缺点】无法实现对象的识别。

function Person(name) {
    var obj = new Object()
    obj.name = name
    obj.getName = function () {
        console.log(this.name)
    }
    return obj
}
const person1 = new Person('lucius')
console.log(person1 instanceof Person) // false
console.log(person1 instanceof Object) // true

稳妥构造函数模式

在构造函数中创建新对象,添加方法,返回新对象。不添加公共属性,方法中也不使用this对象

function person(name){
    var obj = new Object()
    obj.sayName = function(){
        console.log(name)
    }
    return obj
}

const person1 = person('lucius')
person1.sayName() // lucius

person1.name = "alfred"
person1.sayName() // lucius

console.log(person1.name) // alfred