好书精读系列:ECMAScript 创建对象深入理解

233 阅读6分钟

这是我参与更文挑战的第 3 天,活动详情查看: 更文挑战

前言:

作为一名前端人,对于 ECMAScript( JavaScript) 这门语言而言,这可是我们吃饭的家伙,平时工作够用就行,但是一定要不断学习,不断深入了解。所以我打开了 《JavaScript 高级程序设计 (第四版)》,这次,对于对象的创建有了更深入的了解,在这里分享给大家!

ECMAScript 对象的基本介绍,和创建 MDN 和 各大教程、书籍说的都挺好,在这就不赘述了! 上干货

概述

ECMAScript 6开始正式支持类和继承。ES6的类旨在完全涵盖之前规范设计的基于原型的继承模式。不过,无论从哪方面看,ES6的类都仅仅是封装了ES5.1构造函数加原型继承的语法糖而已,在这里,我们就由浅及深,和大家分享 关于对象创建的那些事儿!

工厂模式创建对象

什么是工厂模式?

工厂模式其实就是将创建对象的过程单独封装。它很像我们去餐馆点菜:比如说点一份西红柿炒蛋,我们不用关心西红柿怎么切、怎么打鸡蛋这些菜品制作过程中的问题,我们只关心摆上桌那道菜。在工厂模式里,我传参这个过程就是点菜,工厂函数里面运转的逻辑就相当于炒菜的厨师和上桌的服务员做掉的那部分工作——这部分工作我们同样不用关心,我们只要能拿到工厂交付给我们的实例结果就行了。

  • 工厂模式的目的,就是为了实现无脑传参,就是为了爽!

// 工厂模式创建对象 
function createPerson (name,age,job) {
    let o = new Object()
    o.name = name
    o.age = age
    o.job = job
    o.sayJob = function (){
        console.log(o.job);
    }
    return o
}

let person1 = createPerson("jack","28","996福报大师")

person1.sayJob() // "996福报大师"
let person2 = createPerson("pony","28","王者农药职业大师")


person2.sayJob() // "王者农药职业大师"

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

构造函数模式创建对象

ECMAScript中的构造函数是用于创建特定类型对象的。像Object和Array这样的原生构造函数,运行时可以直接在执行环境中使用。当然也可以自定义构造函数,以函数的形式为自己的对象类型定义属性和方法。比如,前面的例子使用构造函数模式可以这样写:

// 构造函数模式
function Person (name,age,job) {
    this.name = name
    this.age = age
    this.job = job
    this.sayJob = function () {
        console.log(this.job);
    }
    // 逻辑等价于
    // this.sayJob = new Function ("console.log(this.job)")
}
let person1 = new Person("jack","28","996福报大师")
person1.sayJob() // "996福报大师"


let person2 = new Person("pony","28","王者农药职业大师")
person2.sayJob() // "王者农药职业大师"

Person()构造函数代替了createPerson()工厂函数。实际上,Person()内部的代码跟createPerson()基本是一样的,只是有如下区别。

  • 没有显式地创建对象。

  • 属性和方法直接赋值给了this。

  • 没有return。

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

  1. 在内存中创建一个新对象。
  2. 这个新对象内部的[[Prototype]]特性被赋值为构造函数的prototype属性。
  3. 构造函数内部的this被赋值为这个新对象(即this指向新对象)。
  4. 执行构造函数内部的代码(给新对象添加属性)。
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

前面例子中的每个对象都是Object的实例,同时也是Person的实例。

console.log( person2 instanceof Person);  // true
console.log( person2 instanceof Object);	// true

2.构造函数的问题构造函数虽然有用,但也不是没有问题。构造函数的主要问题在于,其定义的方法会在每个实例上都创建一遍。因此对前面的例子而言,person1和person2都有名为sayName()的方法,但这两个方法不是同一个Function实例。我们知道,ECMAScript中的函数是对象,因此每次定义函数时,都会初始化一个对象。逻辑上讲,这个构造函数实际上是这样的:

// 构造函数模式
function Person (name,age,job) {
    this.name = name
    this.age = age
    this.job = job
    // 逻辑等价于
    this.sayJob = new Function ("console.log(this.job)")
}

但创建新Function实例的机制是一样的。因此不同实例上的函数虽然同名却不相等,如下所示:

console.log(person1.sayJob == person2.sayJob) // false

所以没必要定义两个不同的Function实例。况且,this对象可以把函数与对象的绑定推迟到运行时。

// 解决方式1 : 函数定义转移到构造函数外部
function Person (name,age,job) {
    this.name = name
    this.age = age
    this.job = job
    this.sayJob = sayJob
}
sayJob = function () {
    console.log(this.job);
}

在这里,sayJob()被定义在了构造函数外部。在构造函数内部,sayJob属性等于全局sayJob()函数。因为这一次sayJob属性中包含的只是一个指向外部函数的指针,所以person1和person2共享了定义在全局作用域上的sayJob()函数。这样虽然解决了相同逻辑的函数重复定义的问题,但全局作用域也因此被搞乱了,因为那个函数实际上只能在一个对象上调用。如果这个对象需要多个方法,那么就要在全局作用域中定义多个函数。这会导致自定义类型引用的代码不能很好地聚集一起。这个新问题可以通过原型模式来解决。

原型模式创建对象

每个函数都会创建一个prototype属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。实际上,这个对象就是通过调用构造函数创建的对象的原型。使用原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享。

// 解决方式2 : 原型模式

function Person () {}
Person.prototype.name = "xcb"
Person.prototype.sayJob = function () {
    console.log(this.job)
}

sayJob = function () {
    console.log(this.name + this.doing);
}

let person1 = new Person()
person1.name = "Lily"
person1.job = "student"
person1.sayJob()

结语

好了,今天的内容就到这里啦,谢谢 🙏 大家的观看,如果喜欢的话,请为我点个赞吧!

作者:chenuvi

邮箱: chenui@outlook.com