Js Advance --- 面向对象(一)

349 阅读12分钟

对象是JavaScript中一个非常重要的概念,用对象来描述事物,更有利于我们将现实的事物,抽离成代码中某个数据结构

这是因为对象可以将多个相关联的数据封装到一起,更好的描述一个事物

JavaScript其实支持多种编程范式的,包括函数式编程和面向对象编程:

  • JavaScript中的对象被设计成一组属性的无序集合,像是一个哈希表,有key和value组成
  • key是一个标识符名称,value可以是任意类型,也可以是其他对象或者函数类型
  • 如果值是一个函数,那么我们可以称之为是对象的方法

创建方式

Object构造函数

const obj = new Object()
obj.name = 'Klaus'
obj.printName = function() {
  console.log(this.name)
}

字面量

const obj = {
  name: 'Klaus',
  printName() {
    console.log(this.name)
  }
}

属性操作

基本操作

const info = {
  name: 'Klaus',
  age: 18
}

// 获取属性
console.log(info.name)

// 设置属性
info.age = 23

// 删除属性
delete info.age

属性控制

多数情况下,我们的属性都是直接定义在对象内部,或者直接添加到对象内部的

但是这样来做的时候我们就不能对这个属性进行一些限制:比如这个属性是否是可以通过delete删除的?这个属性是否在for-in遍历的时候被遍历出来呢?

如果我们想要对一个属性进行比较精准的操作控制,那么我们就可以使用属性描述符

  • 通过属性描述符可以精准的添加或修改对象的属性
  • 属性描述符需要使用 Object.defineProperty 来对属性进行添加或者修改

Object.defineProperty

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象

可接收三个参数:

  • obj --- 要定义属性的对象

  • prop --- 要定义或修改的属性的名称或 Symbol

  • descriptor --- 要定义或修改的属性描述符 --- 本质上就是一个配置对象 --- 不可省,至少是一个空对象

返回值:

  • 被传递给函数的对象 --- 和obj(第一个参数)本质上是一个对象 --- 所以返回值一般不用接收就可以使用

属性描述符分类

属性描述符的类型有两种:

  • 数据属性(Data Properties)描述符(Descriptor)
  • 存取属性描述符(Descriptor)或(Accessor访问器 Properties)访问器描述符

属性描述符只能是数据描述符或存取描述符中的一种

不可以同时使用

IEqYUO.png

属性描述符
  1. [[Configurable]]: 表示属性是否可以通过delete删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符
    • 当我们直接在一个对象上定义某个属性时,这个属性的[[Configurable]]为true
    • 当我们通过属性描述符定义一个属性时,这个属性的[[Configurable]]默认为false
  2. [[Enumerable]]:表示属性是否可以通过for-in或者Object.keys()返回该属性
    • 当我们直接在一个对象上定义某个属性时,这个属性的[[Enumerable]]为true
    • 当我们通过属性描述符定义一个属性时,这个属性的[[Enumerable]]默认为false
  3. [[Writable]]:表示是否可以修改属性的值
    • 当我们直接在一个对象上定义某个属性时,这个属性的[[Writable]]为true
    • 当我们通过属性描述符定义一个属性时,这个属性的[[Writable]]默认为false
  4. [[value]]:属性的value值,读取属性时会返回该值,修改属性时,会对其进行修改
    • 默认情况下这个值是undefined

value

const info = {}

Object.defineProperty(info, 'age', {
  value: 18
})

console.log(info.age) // => 18

Configurable

const info = {}

Object.defineProperty(info, 'age', {
  value: 18,
  configurable: false
})

info.age = 23 // 静默错误 -- 无效代码
console.log(info.age) // => 18
const info = {}

Object.defineProperty(info, 'age', {
  value: 18,
  configurable: true
})

Object.defineProperty(info, 'age', {
  value: 23
})

console.log(info.age) // => 23
const info = {}

Object.defineProperty(info, 'age', {
  value: 18,
  configurable: false
})

Object.defineProperty(info, 'age', {
  value: 23
})

console.log(info.age) // => error 不是静默错误,直接报错

Enumerable

const info = {}

Object.defineProperty(info, 'age', {
  value: 18,
  enumerable: false
})

console.log(info)  // => {}
// 但是可以通过属性来取值
console.log(info.age) // => 18
const info = {
  name: 'Klaus'
}

Object.defineProperty(info, 'age', {
  value: 18,
  enumerable: false
})

for (let key in info) {
  console.log(key) // => 只会输出Klaus,不会输出age
}
const info = {
  name: 'Klaus'
}

Object.defineProperty(info, 'age', {
  value: 18,
  enumerable: false
})

console.log(Object.keys(info)) // => ['name']

Writable

const info = {
  name: 'Klaus'
}

Object.defineProperty(info, 'age', {
  value: 18,
  writable: false
})

console.log(info.age) // => 18
info.age = 23 // 静默修改
console.log(info.age) // => 18
存取描述符
  • [[Configurable]]: 和数据属性描述符是一致
  • [[Enumerable]]: 和数据属性描述符是一致
  • [[get]]:获取属性时会执行的函数。默认为undefined
  • [[set]]:设置属性时会执行的函数。默认为undefined

示例

const info = {
  name: 'Klaus',
  _age: 18
}

// get/set方法内部可以使用this,值为传入的对象,在这里就是info
Object.defineProperty(info, 'age', {
  get() {
    return this._age
  },

  set(value) {
    this._age = value
  }
})

console.log(info.age) // => 18
info.age = 23
console.log(info.age) // => 23

作用

  1. 避免暴露私有属性
const info = {
  name: 'Klaus'
}

// 私有属性的属性名一般使用下划线开头
Object.defineProperty(info, '_age', {
  writable: true
})

Object.defineProperty(info, 'age', {
  get() {
    return this._age
  },

  set(value) {
    this._age = value
  },
  enumerable: true
})

console.log(info) // => { name: 'Klaus', age: [Getter/Setter] }

console.log(info.age) // => undefined
info.age = 23
console.log(info.age) // => 23
  1. 对属性值进行劫持

    只能对属性值进行劫持,如果要对整个对象进行劫持,需要使用proxy

const info = {
  name: 'Klaus',
  _age: 18
}
Object.defineProperty(info, 'age', {
  get() {
    console.log('获取值之前进行的相关操作')
    // 注意: 返回的值和属性不能是同一个,否则会无限调用get方法
    return this._age
  },

  set(value) {
    console.log('设置值之前进行的相关操作')
    this._age = value
  }
})

info.age = 23
console.log(info.age)

Object.defineProperties

Object.defineProperties() 方法直接在一个对象上定义 多个 新的属性或修改现有属性,并且返回该对象

参数一: 需要添加属性的那个对象

参数二: 属性对象

  • key ---> 属性名
  • value ---> 属性对应的描述对象
const info = {
  _age: 18
}

Object.defineProperties(info, {
  name: {
    value: 'Klaus',
    writable: true,
    enumerable: true
  },

  age: {
    enumerable: true,
    get() {
      return this._age
    },
    set(v) {
      this._age = v
    }
  }
})

console.log(info)

get/set的简写

const info = {
  _age: 18,

  // 这种设置方式下age的configuration和enumerate的值默认都是true
  get age() {
    return this._age
  },

  set age(v) {
    this._age = v
  }
}

console.log(info.age) // => 18
info.age = 25
console.log(info.age) // => 25

Object方法补充

getOwnPropertyDescriptor --- 获取对象的某一个属性的属性描述符

console.log(Object.getOwnPropertyDescriptor(info, 'age'))

getOwnPropertyDescriptors --- 获取对象的所有属性的属性描述符

console.log(Object.getOwnPropertyDescriptors(info))

seal --- 密封对象(禁止配置) ---> 对象所有属性的configuration的值皆设置为false

const info = {
  _age: 18,

  get age() {
    return this._age
  },

  set age(v) {
    this._age = v
  }
}

Object.seal(info)

delete info.age
console.log(info.age) // => 18

freeze --- 冻结对象(禁止修改) ---> 对象所有属性的writable的值皆设置为false

const info = {
  _age: 18,

  get age() {
    return this._age
  },

  set age(v) {
    this._age = v
  }
}

Object.freeze(info)

info.age = 23
console.log(info.age) // => 18

preventExtenstions --- 禁止扩展 ---> 禁止为对象添加新的属性

const info = {
  _age: 18,

  get age() {
    return this._age
  },

  set age(v) {
    this._age = v
  }
}

Object.preventExtensions(info)

info.height = 1.80

// height属性添加静默失败
console.log(info) // => { _age: 18, age: [Getter/Setter] }

// 判断info对象是否是可扩展的
console.log(Object.isExtensible(info)) // => false

批量创建对象

如果我们现在希望创建一系列的对象,这一系列对象的结构是类似的,只要少量的区别或者仅仅只有值的不同

如果继续使用字面量创建的方式或new Object方式进行创建,就需要编写很多重复的代码

工厂模式

工厂模式其实是一种常见的设计模式

通常我们会有一个工厂方法,通过该工厂方法可以根据我们传入的配置数据,返回我们所需要的对象

function createPerson(name, age) {
  const per = {
    name,
    age
  }

  per.running = () => console.log('running')

  return per
}

// createPerson可以根据我们传入的信息生成我们所需要的对象
const p1 = createPerson('Klaus', 23)
const p2 = createPerson('Alex', 32)
const p3 = createPerson('Jhon', 17)

console.log(p1)
console.log(p2)
console.log(p3)
p1.running()

工厂函数虽然提取了批量创建对象时候的重复代码,但是工厂函数依旧存在如下两个问题:

  • 所有的工厂函数,无论返回的是什么类型的对象,其类型值都是Object,无法进行更细的区分
  • 工厂函数中定义的方法只能添加到某一个具体的对象实例上,无法很方便的添加到原型链中

构造函数

构造函数也称之为构造器(constructor),通常是我们在创建对象时会调用的函数

但是JavaScript中的构造函数有点不太一样:

  • JS中构造函数也是一个普通的函数,从表现形式来说,和千千万万个普通的函数没有任何区别
  • 如果一个函数使用new操作符来调用了,那么这个函数就称之为是一个构造函数
  • 所以为了区分,约定俗成下,构造函数的首字母一般大写,如果有多个单词组成使用大驼峰命名法
function Foo() {
  console.log('foo')
}

// 构造函数本质上就是一个普通的函数
// 可以和普通函数一样进行调用
Foo() // => foo

// 当我们使用new关键字来对某一个函数进行调用的时候
// 这个函数就被称之为构造函数
new Foo() // => foo

// 如果构造函数没有参数, 后边的括号原则上可以省略 --- 不推荐
new Foo // => foo

如果一个函数被使用new操作符调用了,那么它会执行如下操作:

  • 在内存中创建一个新的对象(空对象)

  • 对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性

    ( 对象的__proto__的值会被赋值为对象.prototype所对应的值)

  • 构造函数内部的this,会指向创建出来的新对象

  • 执行函数的内部代码(函数体代码)

  • 如果构造函数没有返回非空对象,则返回创建出来的新对象

function Person(name, age) {
  this.name = name
  this.age = age
}

const per = new Person('Klaus', 23)
// 此时可以发现的是输出的对象具有了类型,类型为构造函数对应的函数名
// 这是构造函数相比工厂函数最大的优势之一
console.log(per) // => Person { name: 'Klaus', age: 23 }

tips: 如果构造函数没有指定返回值,默认返回的就是构造函数对应的实例对象

但是如果构造函数指定了实际的返回值,此时分两种情况

  1. 如果返回值为基本数据类型,显示指定的返回值会被忽略,而返回对应的实例对象
  2. 如果返回值是引用数据类型,那么显示指定的返回值会覆盖默认的返回值

如果返回值为基本数据类型,显示指定的返回值会被忽略,而返回对应的实例对象

function Person(name, age) {
  this.name = name
  this.age = age
  return 123
}

const per = new Person('Klaus', 23)
console.log(per) // => Person { name: 'Klaus', age: 23 }

如果返回值是引用数据类型,那么显示指定的返回值会覆盖默认的返回值

function Person(name, age) {
  this.name = name
  this.age = age
  return {}
}

const per = new Person('Klaus', 23)
console.log(per) // => {}

虽然相比工厂函数而言,构造函数创建的对象已经拥有了自己实际的数据类型

但是这里依旧存在一个比较大的问题, 它在于我们需要为每个对象的函数去创建一个函数对象实例

function Person(name, age) {
  this.name = name
  this.age = age

  // running方法会存在于每一个Person的实例中
  // 但是对于所有的Person的实例而言,running方法都是一样的, 所以这是重复的冗余代码
  this.running = () => console.log('running')
}

原型

JavaScript当中每个对象都有一个特殊的内置属性 [[prototype]],这个特殊的对象指向另外一个对象

任意一个对象在被创建的时候,都会有一个对象,这个对象被称之为该对象的原型对象

const obj = {}

// 我们可以使用对象实例的__proto__属性去访问原型对象
// 但这个属性并不推荐我们直接去调用,这个获取原型的属性就被称之为隐式原型
console.log(obj.__proto__) // => {}

// ES6后,提供了getPrototypeOf的方法来帮助我们获取对象的原型
console.log(Object.getPrototypeOf(obj)) // => {}

属性查找

我们通过引用对象的属性key来获取一个value时,它会触发 [[Get]]的操作

这个操作会首先检查该属性是否有对应的属性,如果有的话就使用它

如果对象中没有改属性,那么会访问对象[[prototype]]内置属性指向的对象上的属性

const obj = {}
const prototype = Object.getPrototypeOf(obj)

prototype.name = 'Klaus'

console.log(obj.name) // => Klaus

显式原型

function Foo() {}

// 函数是一种特殊的可执行对象
// 所以在函数对象上(并不一定是构造函数)
// 有一个属性叫做prototype,对应的值是一个对象,
// 在使用new关键字创建对象的时候
// 内部this的__proto__会指向prototype所指向的对象
// 也就是说所有Foo实例的__proto__和函数对象的prototype的指向是一样的
// 这也就是为什么prototype一般被称之为显式原型对象
console.log(Foo.prototype) // => {}

const foo = new Foo()

console.log(foo.__proto__ === Foo.prototype) // => true

constructor

虽然直接打印原型对象的时候,输出的结果是一个空对象

但是事实上原型对象上面是有一个属性的: constructor

默认情况下原型上都会添加一个属性叫做constructor,这个constructor指向当前的函数对象

IEssFw.png

重写原型对象

如果我们需要在原型上添加过多的属性,通常我们会重新整个原型对象

function Foo() {}

Foo.prototype = {
  name: 'Klaus',
  age: 23,

  eatting() {
    console.log('eatting')
  }
}

但是我们这里相当于给prototype重新赋值了一个对象, 那么这个新对象的constructor属性, 会指向Object构造函 数, 而不是Person构造函数了

所以此时我们需要手动指定constructor属性对应的值

function Foo() {}

Foo.prototype = {
  name: 'Klaus',
  age: 23,

  eatting() {
    console.log('eatting')
  },

  constructor: Foo
}

但是上述添加constructor属性值的方式会造成constructor的[[Enumerable]]特性被设置了true

因为默认情况下, 原生的constructor属性是不可枚举的,此时就可以使用Object.defineProperty()

function Foo() {}

Foo.prototype = {
  name: 'Klaus',
  age: 23,

  eatting() {
    console.log('eatting')
  }
}

Object.defineProperty(Foo.prototype, 'constructor', {
  writable: true,
  configurable: true,
  value: Foo
})

构造函数和原型组合

综上, 我们实际在创建对象的时候,其实是使用的构造函数和原型组合的方式来为我们批量创建对应的对象

function Person(name, age) {
  this.name = name
  this.age = age
}

Person.prototype.eatting = () => console.log('eatting')

const p1 = new Person('Klaus', 23)
const p2 = new Person('Alex', 32)

console.log(p1.__proto__ === Person.prototype) // => true
console.log(p1.__proto__ === Person.prototype) // => true

console.log(p1.eatting === p2.eatting) // => true