对象

创建自定义对象

  • new 关键字 + Object 构造函数
  • 对象字面量

属性的类型

属性分为:数据属性和访问器属性

  1. 数据属性

包含一个保存数据值的位置。

数据属性有 4 个特性描述它们的行为。

  • [[Configurable]]: 表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改成访问器属性。默认为 true
  • [[Enumerable]]: 表示属性是否可以通过 for-in 循环返回。默认为 true
  • [[Writable]]: 表示属性的值是否可以被修改。默认为 true
  • [[Value]]: 包含属性实际的值。默认为 undefined

要修改属性的默认特性,就必须使用 Object.defineProperty() 方法。

Object.defineProperty() 接收三个参数:要给其添加属性的对象、属性的名称和一个描述符对象。 描述符对象上的属性可以包含:configurable、enumerable、writable、value。

let person = {}
Object.defineProperty(person, 'name', {
  writable: false,
  value: 'dan'
})

console.log(person.name) // dan
person.name = 'ge'
console.log(person.name) // ge

一个属性被定义为不可配置之后,就不能再变回可配置的了。

  1. 访问器属性

访问器属性不包含数据值。它们包含一个获取函数和一个设置函数,但不是必需的。

访问器属性有 4 个特性描述它们的行为:

  • [[Configurable]]: 表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为数据属性。默认为 true
  • [[Enumerable]]: 表示属性是否可以通过 for-in 循环返回。默认为 true
  • [[Get]]: 获取函数,在读取属性时调用。默认 undefined
  • [[Set]]: 设置函数,在写入属性时调用。默认 undefined

访问器属性不能直接定义,必须使用 Object.defineProperty()。

获取函数和设置函数不一定都要定义。指定以获取函数意味着属性是只读的,尝试修改属性会被忽略。在严格模式下,尝试写入只定义了获取函数的属性会抛出错误。 只有一个设置函数的属性是不能读取的,非严格模式下读取会返回 undefined,严格模式下会抛出错误。

在不支持 Object.defineProperty() 的浏览器中没办法修改 [[Configurable]] 和 [[Enumerable]]

定义多个属性

Object.defineProperties() 方法可以通过多个描述符一次性定义多个属性。 接受两个参数:要为之添加或修改属性的对象和另一个描述符对象,其属性与要添加或修改的属性一一对应。

let book = {}
Object.defineProperties(book, {
  year_: {
    value: 2021,
  },
  edition: {
    value: 1,
  },

  year: {
    get() {
      return this.year_
    },
    set(newValue) {
      if (newValue > 2021) {
        this.year_ = newValue
        this.edition += newValue - 2021
      }
    }
  }
})

读取属性的特性

使用 Object.getOwnPropertyDescriptor() 方法可以取得指定属性的属性描述符。

接受两个参数:属性所在的对象和要取得其描述的属性名。

返回值:一个对象

let book = {}
Object.defineProperties(book, {
  year_: {
    value: 2021,
  },
  edition: {
    value: 1,
  },
  year: {
    get: function () {
      return this.year
    },
    set: function(newValue) {
      if (newValue > 2021) {
        this.year_ = newValue
        this.edition += newValue - 2021
      }
    }
  }
})

let descriptor = Object.getOwnPropertyDescriptor(book, 'year_')
console.log(descriptor.value) // 2021
console.log(descriptor.configurable) // false
console.log(typeof descriptor.get) // 'undefined'
let descriptor = Object.getOwnPropertyDescriptor(book, 'year')
console.log(descriptor.value) // undefined
console.log(descriptor.configurable) // false
console.log(typeof descriptor.get) // 'function'

ECMAScript 2017 新增了 Object.getOwnPropertyDescriptors() 静态方法。这个方法实际上会在每个自有属性上调用 Object.getOwnPropertyDescriptor() 并在一个新对象中返回它们。

let book = {}
Object.defineProperties(book, {
  year_: {
    value: 2021
  },
  edition: {
    value: 1
  },
  year: {
    get: function () {
      return this.year_
    },
    set: function (newValue) {
      if (newValue > 2021) {
        this.year_ = newValue;
        this.edition += newValue - 2021
      }
    }
  }
})
console.log(Object.getOwnPropertyDescriptors(book))
// {
//   edition: {
//     configurable: false,
//     enumerable: false,
//     value: 1,
//     writable: false
//   },
//   year: {
//     configurable: false,
//       enumerable: false,
//       get: f(),
//       set: f(newValue),
//   },
//   year_: {
//     configurable: false,
//     enumerable: false,
//     value: 2021,
//     writable: false
//   }
// }

合并对象

把源对象所有的本地属性一起复制到目标对象上。

Object.assign() 接受一个目标对象和一个或多个源对象作为参数。将每个源对象中可枚举和自由属性复制到目标对象上。

可枚举属性,Object.propertyIsEnumerable() 返回 true

自由属性,Object.hasOwnProperty() 返回 true

以字符串和符号为键的属性会被复制。

Object.assign() 会使用源对象上的 [[Get]] 取得属性的值,使用目标对象伤的 [[Set]] 设置属性的值。

实际上是浅复制。

对象标识及相等判定

Object.is() 判断是否相等

Object.is(true, 1) // false
Object.is({}, {}) // false
Object.is('2', 2) // false

Object.is(+0, -0) // false
Object.is(+0, 0) // true
Object.is(-0, 0) // false

Object.is(NaN, NaN) // true

增强的对象语法

  1. 属性值简写
  2. 可计算属性
  3. 简写方法名

对象解构

解构在内部使用函数 ToObject() 把源数据结构转换为对象。原始值会被当成对象。null、undefined 不能被解构。

创建对象

Object 构造函数 或者 对象字面量。

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.sayName = function() {
    console.log(this.name)
  }
  return o
}
let person1 = createPerson("Nicholas", 29, "Software Engineer")
let person2 = createPerson("Greg", 27, "Doctor")

构造函数模式

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")
person1.sayName() // Nicholas
person2.sayName() // Greg

使用 new 操作符创建 实例 的方式会执行如下操作:

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

并且每个实例的 constructor 指向构造函数 Person

实例化时,构造函数如果没有参数,那么括号可加可不加。

  1. 构造函数也是函数
  2. 构造函数的问题:定义的方法会在每个实例上都创建一遍,所以可以把函数定义放在构造函数外部。

原型模式

每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。

好处:通过调用构造函数创建的对象的原型上面定义的属性和方法可以被对象实例共享。

实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有。

通过 hasOwnProperty() 可以查看访问的是实例属性还是原型属性。

in 操作符,单独使用可以通过对象访问指定属性时返回 true,无论该属性是在实例上还是在原型上。

// 返回 TRUE,表示 非实例属性,但是是原型属性
function hasPrototypeProperty(object, name) {
  return !object.hasOwnProperty(name) && (name in object)
}

属性枚举顺序

取决于浏览器引擎

  • for-in
  • Object.keys()

先枚举数值键,再以插入顺序枚举字符串和符号键

  • Object.getOwnPropertyNames()
  • Object.getOwnPropertySymbols()
  • Object.assign()

开发中通常不单独使用原型模式,多个实例间共享引用值时,会出现难以预料的后果。所以每个实例应该有属于自己的引用值的属性副本。

继承

原型链

ECMA-262 把原型链定义为 ECMAScript 的主要继承方式。

基本思想:通过原型集成多个引用类型的属性和方法。

原型链:每个构造函数都有一个原型对象,原型有一个属性指向构造函数,实例有一个内部指针指向原型。若原型是另一个类型的实例,那么就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样在实例和原型之间构造了一条原型链。

构造函数

基本思路:在子类构造函数中调用父类构造函数。使用 apply() 和 call() 方法以新创建的对象为上下文执行构造函数。

问题:必须在构造函数中定义方法,不能复用。子类也不能访问父类原型上定义的方法。

组合继承

综合了原型链和构造函数的优点。

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

组合继承 弥补了原型链和构造函数的不足,也保留了 instanceof 操作符和 isPrototypeOf() 方法识别合成对象的能力。

缺点:父类构造函数始终会被调用两次,一次是在创建子类原型时调用,另一次是在子类构造函数中调用

原型式继承

基本思路:首先有一个对象,在它的基础上再创建一个新对象。

function object(o) {
  function F() {}
  F.prototype = o
  return new F()
}
  • 创建一个临时构造函数
  • 将传入的对象赋值给这个构造函数的原型
  • 返回临时构造函数的一个实例

ECMAScript 5 增加了 Object.create() 方法将原型式继承的概念规范化了。

Object.create() 接受两个参数,第一个作为新对象原型的对象,第二个参数可选,给新对象定义额外属性的对象。

原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。

寄生式继承

基本思路:创建一个实现集成的函数,以某种形式增强对象,然后返回这个对象。

function createAnother(o) {
  let clone = Object.create(o)
  clone.sayHi = function() {
    console.log('hi')
  }
  return clone
}

寄生式组合继承

基本思路:不通过父类构造函数给子类原型赋值,而是取得父类原型的一个副本。

基本模式

function inheritPrototype(subType, superType) {
  let prototype = Object.create(superType.prototype)
  prototype.constructor = subType
  subType.prototype = prototype
}

这样就只调用了一次父类构造函数

ECMAScript 6 新引入的 class 关键字具有正式定义类的能力。是新的基础性语法糖结构。

表面上看起来可以支持正式的面向对象编程,但实际上使用的仍然是原型和构造函数的概念。

类定义

两种方式:类声明和类表达式

// 类声明
class Person {}

// 类表达式
const Animal = class {}

类定义不能提升,受块作用域限制。

默认情况下,类定义的代码都在严格模式下执行。

ECMAScript 中没有正式的类这个类型。大体上 类算是一种特殊函数。声明一个类之后,通过 typeof 操作符检测类标识符,表明它是一个函数。

使用 extends 关键字 实现单继承。

ES6 给类构造函数和静态方法添加了内部特性[[HomeObject]],这个特性是一个指针,指向定义该方法的对象。这个指针是自动赋值的,而且只能在 JavaScript 引擎内部访问。super 始终会定义为 [[HomeObject]] 的原型

类构造函数中,不能在调用 super() 之前引用 this

派生类中显式定义了构造函数,那么必须在其中调用 super() 或者 返回一个对象

抽象基类:可供其他类继承,但本身不会被实例化。

class Vehicle {
  constructor() {
    console.log(new.target) // new.target 保存通过 new 关键字调用的类或函数
    if (new.target === Vehicle) {
      throw new Error('Vehicle cannot be directly instantiated')
    }
  }
}

// 派生类
class Bus extends Vehicle {}

new Bus() // class Bus {}
new Vehicle() // class Vehicle {}
// Error: Vehicle cannot be directly instantiated
class Vehicle {
  constructor() {
    if (new.target === Vehicle) {
      throw new Error('Vehicle cannot be directly instantiated')
    }
    if (!this.foo) {
      throw new Error('Inheriting class must define foo()')
    }
    console.log('success!')
  }
}
// 派生类
class Bus extends Vehicle {
  foo() {}
}
// 派生类
class Van extends Vehicle {}

new Bus() // success!
new Van() // Error: Inheriting class must define foo()