讲清楚之 javascript 对象(一)

有了前面几节的知识,这一节咱们理解起来就要轻松不少。在 javascript 里函数也是对象,浏览器的全局上下文也是对象, key - value 的身影在代码里比较常见,合理的使用对象多维度、可扩展的特性能够为开发中带来不少乐趣。javascript

若是知识存在盲区,则实际开发中就会就会应为评估不足,模型设计不合理出现各类问题, 小则打打补丁、模块API从新设计,作兼容处理。 大则是关键数据维度没法知足应用场景, 就须要费事费力的进行架构调整或者重构了。java

下面咱们来梳理一下 javascript 对象的表现方式和特色,过于细节的知识就不梳理了。设计模式

JavaScript 的设计是一个简单的基于对象的范式。一个对象就是一系列属性的集合,一个属性包含一个属性名和一个属性值。一个属性的值能够是函数,这种状况下属性也被称为 方法。除了浏览器里面预约义的那些对象以外,咱们也能够定义本身的对象。熟悉 javascript 的语法特性,合理的设计数据模型,建立灵活、不含糊的自定义对象可以提升 javascript 的运行效率。

字面量对象

使用字面量方式建立对象占据了大多数开发场景,字面量对象示例:数组

let foo = {
    a: 1,
    b: '1234',
    c: function () {
        console.log(this.a + this.b)
    }
}
let foo1 = {
    a: 666,
    b: 'hi',
    c: function () {
        console.log(`${this.b}, ${this.a}`)
    }
}
foo.c() // '11234'
foo1.c() // 'hi, 666'

对象字面量的特色主要是直观、简单灵活,每个key、value在编码阶段就是肯定的。浏览器

使用对象字面量的方式来建立对象的缺点是,当咱们须要建立多个相同对象时必须为每一个对象在源代码中编写变量和方法。当这样的相同内容的对象不少时就是一场灾难。因而咱们发明了不少其余建立对象的方式,下面进一步探讨。网络

工厂模式

工厂模式建立对象示例:架构

let createFoo = function (a, b, c) {
    let o = new Object()
    o.a = a
    o.b = b
    o.c = c
    return o
}
let foo = createFoo(1, '1234', function(){
    console.log(this.a + this.b)
})
let foo1 = createFoo(666, 'hi', function(){
    console.log(`${this.b}, ${this.a}`)
})

foo.c() // '11234'
foo1.c() // 'hi, 666'

所谓工厂模式就是对象的建立就像'商品'经过工厂按照标准化的流程被加工出来。app

上面就是一个工厂函数的栗子,执行 createFoo 函数时先建立一个对象 o,而后把传递进来的实参添加到 o 上面,最后返回对象 o。这样每次执行 createFoo 函数都会返回一个新的对象,当咱们须要1000个类似对象时 createFoo 就为咱们在内部生成了1000个独立的对象 o。经过对这个栗子的分析会发现: 工厂函数在进行大批量对象建立时对资源的消耗比较大,同时因为每次都返回的是一个新对象,咱们就没办法判断对象的类型。函数

工厂函数与字面量方式建立对象相比,优点就是不用在编码阶段建立大批量类似结构的对象,而这一系列的建立工做都是在运行阶段建立的。每次建立实例时都要建立实例对应的全部属性和方法,因此工厂函数一样存在建立N个实例须要建立N个属性、方法的问题。this

工厂函数建立实例同时也面临实例类型的问题:

foo instanceof createFoo // false
foo1 instanceof createFoo // false

// 返回的对象是构造函数 Object 的实例
foo instanceof Object // true
foo1 instanceof Object // true
为何实例函数不相等呢?
在 JavaScript 中 objects 是一种引用类型。两个独立声明的对象永远也不会相等(由于变量 foo 和 foo1 指向的堆地址不一样),即便他们有相同的属性,只有在比较一个对象和这个对象的引用时,才会返回true.
let too = {
    a: 1
}
let too1 = {
    a: 1
}
let too2 = too1

too == too1 // false
too === too1 // false

too1 == too2 // true
too1 ===too2 // true

构造函数

构造函数方式建立自定义对象,就是利用函数中构造函数原形实例对象之间的关系来封装私有属性、公有属性:

function Foo (a, b, c) {
    this.a = a
    this.b = b
    this.c = c
}
let foo1 = new Foo(1, '1234', function(){
    console.log(this.a + this.b)
})
let foo2 = new Foo(666, 'hi', function(){
    console.log(`${this.b}, ${this.a}`)
})

// foo一、foo2 是 Foo 的实例
foo1 instanceof Foo // true
foo2 instanceof Foo // true

构造函数的实现看着要简单不少,也能经过实例判断出类型。

构造函数的执行逻辑:

构造函数初始化阶段首先会向上下文栈中压入一个上下文,接着在变量对象建立的时候会收集实参,初始化函数内部的变量申明、肯定 this 的指向、肯定做用链。将实参的值分别拷贝给变量a、b、c。而后像普通函数同样进入执行阶段,执行函数内部语句.

构造函数就是函数 既然构造函数就是普通函数, 那么为什在函数前面加一个 new 就能实例化并返回一个对象呢?

咱们来建立一个模拟构造函数加深理解,没错是建立一个构造函数(思路来源于网络, 无耻的偷过来了ɖී؀ීϸ)。

// 假设咱们建立一个汽车对象类型, car函数
function Car(make, model, year) {
    this.make = make
    this.model = model
    this.year = year
    this.drive = function (name) {
        console.log(`${name} drives the ${this.model} ${this.make}`)
    }  
}

// 将函数以参数形式传入
function New(func) {
    // 声明一个中间对象,该对象为最终返回的实例
    let res = {}
    if (func.prototype !== null) {
        // 将实例的原型指向构造函数的原型
        res.__proto__ = func.prototype
    }
    // ret为构造函数执行的结果,这里经过apply,将构造函数内部的this指向修改成指向res,即为实例对象
    var ret = func.apply(res, Array.prototype.slice.call(arguments, 1))
    // 当咱们在构造函数中明确指定了返回对象时,那么new的执行结果就是该返回对象
    if ((typeof ret === "object" || typeof ret === "function") && ret !== null) {
        return ret
    }
    // 若是没有明确指定返回对象,则默认返回res,这个res就是实例对象
    return res
}
// 经过new声明建立实例,这里的p1,实际接收的正是new中返回的res
let mycar  = New(Car, "Tesla", "Model X", 2018)
mycar.drive('小丸子')
console.log(mycar.make);

// mycar 是 Car 的实例
mycar instanceof Car // true

let mycar = new Car(...) 实例化对象的方式看做是let mycar = New(Car, "Tesla", "Model X", 2018) 的一种简单的语法糖写法。

代码 new Car(...) 执行时,会发生如下事情:

  1. 一个继承自 Car.prototype 的新对象被建立。
  2. 使用指定的参数调用构造函数 Car ,并将 this 绑定到新建立的对象。new Car 等同于 new Car(),也就是没有指定参数列表,Car 不带任何参数调用的状况。
  3. 构造函数返回的对象就是 new 表达式的结果。若是构造函数没有显式返回一个对象,则使用步骤1建立的对象。(通常状况下,构造函数不返回值,可是用户能够选择主动返回对象,来覆盖正常的对象建立步骤)

实例类型没法判断的问题, 经过构造函数的方式来建立对象完美的解决了。可是构造器函数存在和工厂函数同样的问题:每次建立一个实例对象时都会在内部新建一个中间对象,实例方法也会建立N次,这样就存在没必要要的内层消耗。

原型与构造函数组合

在上面Car构造函数的栗子中,当建立100个 Car 的实例时内部复制了100次 drive 函数。 虽然每一个 drive 函数的功能同样,可是因为分别属于不一样的实例就每次都分配独立的内存空间。

相同的功能函数怎么忍受得了重复建立。回忆以前咱们在原型一节讲到的,每一个函数存在prototype 属性,经过该属性指向本身的原型对象。那咱们能够在函数的原型上作文章,将实例公共的属性和方法挂载在原型上。实例经过__ptoto__属性指向了构造函数的原型,从而让构造函数的原型对象在各个实例的原型链上,因而咱们经过构造函数的原型来实现公有属性和方法的封装,且只会建立一次。

仍是上面 Car的栗子:

function Car(make, model, year) {
    this.make = make
    this.model = model
    this.year = year
}
Car.prototype.drive = function (name) {
    console.log(`${name} drives the ${this.model} ${this.make}`)
}

let mycar  = new Car( "Tesla", "Model X", 2018)
mycar.drive('小丸子')

上面的栗子也还能够写成这样子:

function Car(make, model, year) {
    this.make = make
    this.model = model
    this.year = year
}

Car.prototype = {
    constructor: Car,
    drive: function () {
        console.log(`${name} drives the ${this.model} ${this.make}`)
    }
}

let mycar  = new Car( "Tesla", "Model X", 2018)
mycar.drive('小丸子')

两种写法是等价的,须要注意的是后一种至关于建立一个新对象并赋值给了构造函数Car的原型,若是不将新原型的constructor重现指向构造函数,则会致使构造函数Car的实例类型判断出错(instanceof Car 为 false).

不一样的实现方法都有各自的使用场景。同时对象的实现方式又与数据维度以及另一个话题 设计模式有关。咱们使用原型与构造函数组合模式就可以解决不少问题。

关于 javascript 的各类模式能够参考:

Javascript设计模式

相关文章
相关标签/搜索