js红宝石 第8章-对象,类与面向对象编程

ECMA-262把对象定义为一组属性的无需集合.

这意味着对象就是一组没有特定顺序的值,对象的每一个属性和方法都由一个名称来标识,这个名称映射到一个值

所以可以把对象理解成一个散列表,内容就是一对对键值对,其值可能是数据或者函数

8.1 理解对象

创建对象的方式通常是创建Object的实例,然后再给他添加新的属性和方法

但是对象字面量更加流行

      let person = {
        name: 'zs',
        age: 20
      }

类中的属性和方法决定了他们在JS中的行为

8.1.1 属性的类型

属性分为两种:

1.数据属性

数据属性包含一个数据值的位置,有4个特征去描述行为

[[Configurable]]: 表示属性是否可以通过delete删除并且重新定义

[[Enumerable]]: 表示属性是否可以通过for-in循环返回

[[Wirtable]]: 属性值是否能被修改

[[Value]]: 表示属性实际的值

      let person = {
        name: 'zs',
        age: 20
      };
      Object.defineProperty(person, "address" ,{
        value: 'bj',
        writable: false
      })
      console.log(person.address);

2.访问器属性

访问器属性不包含数据值,相反,它们包含一个getter()和setter()函数,在读取属性值时,就会调用这个函数,返回有效值,写入同理

包含4个特征属性:

[[Configurable]]: 表示属性是否可以通过delete删除并且重新定义

[[Enumerable]]: 表示属性是否可以通过for-in循环返回

[[Get]]: 获取函数

[[Set]]: 设置函数

8.1.2 定义多个属性

ECMAScript提供了Object.defineProperty() 可以通过描述符一次定义多个属性

8.1.3 读取属性的特性

通过 Object.getOwnPropertyDescriptors() 可以得到一个对象,对象的属性就是特征值

接受两个参数  属性所在对象和属性名

8.1.4 合并对象

通过 Object.assign()实现对象的合并

      let dest, src, result

      dest = { name: 'dest' }
      src = { id: 'src' }

      result = Object.assign(dest, src)

      //会修改目标对象,也会返回修改后的目标对象
      console.log(dest === result)
      console.log(dest === src)
      console.log(dest)
      console.log(result)

 

8.1.5 对象相等及相等判定

Object.is() 和 === 很像

但是Object.is()考虑了一些 === 的边界情况

8.1.6 增强的对象语法

1.属性值简写

      let name = 'zs'

      let person = {
        // 简写属性名只要写变量名就会被自动解析成同名属性键
        name
      }

2. 可计算属性

      
      let nameKey = 'name'

      let person = {
        // [] 告诉JS把它当成表达式而不是字符串
        [nameKey] : 'zs'
      }

3.简写方法名

      let nameKey = 'name'

      let person = {
        [nameKey] : 'zs',

        sayname: function(name){
          console.log(`My name is ${name}`);
        },

        // 简写方法名
        sayname(name){
          console.log(`My name is ${name}`);
        }
      }

8.1.7 对象解构

      let person = {
        name: 'zs',
        age: 20,
      }

      // 对象结构
      let { name: personName, age: PersonAge } = person

      (
        // 如果让变量直接使用属性名,可以简写
        // 如果给事先定义过的变量赋值,要用括号包裹
        ({ name, age } = person)
      )

      (
        // 可以定义默认值
        ({ name, job = 'engineer' } = person)
      )

1.嵌套解构

可以通过嵌套解构去匹配嵌套的属性

      let person = {
        name: 'zs',
        age: 20,
        job: {
          title: 'eng',
        },
      }

      //嵌套解构
      let { job: { title } } = person

但是在外层属性没有定义的情况下不能使用嵌套解构

2.部分解构

涉及多个属性的解构赋值是一个输出无关的序列化操作,如果一个解构表达式有错误,那只有前面的会成功

3.参数上下文匹配

在函数的参数列表中可以使用解构赋值

      let person = {
        name: 'zs',
        age: 20,
        job: {
          title: 'eng',
        },
      }

      function say({name,age}){
        console.log(`My name is ${name},my age is ${age}`);
      }

      say(person);

8.2 创建对象

8.2.1 概述

ES6开始正式支持类和继承.但是仅是封装了ES5.1构造函数加原型继承的语法糖而已

8.2.2 工厂模式

用于抽象特定对象的设计过程

      function createperson(name,age,job){
        let o = new Object()
        o.name = name
        o.age = age
        o.job = job
        o.sayname = function(){
          console.log(name);
        }
        return o
      }

      let person1 = createperson('zs',20,'stu')

工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)

8.2.3 构造函数模式

构造函数是用于创建特定类型的对象的

      function Person(name,age,job){
        this.name = name
        this.age = age
        this.job = job
        this.sayname = function(){
          console.log(name);
        }
      }

      let person1 = new Person('zs',20,'stu')

和工厂模式的区别:

1.没有显式地创建对象

2.属性和方法直接赋值给了this

3.没有return

要注意函数名首字母大写,这是惯例

构造函数也是函数

构造函数的问题

构造函数定义的方法会在每个实例上都创建一遍,因为每个实例都有Function实例,定义方法实际上是给每个Function实例创建属性

相当于

      function Person(name,age,job){
        this.name = name
        this.age = age
        this.job = job
        this.sayname = new function(){
          console.log(name);
        }
        // 逻辑等价
        this.sayname = new Function("console.log(name)")
      }

解决方法:把函数定义转移到函数外部

      function Person(name,age,job){
        this.name = name
        this.age = age
        this.job = job
        this.sayname = sayname
      }
      
      
      function sayname(){
        console.log(name)
      }

8.2.4 原型模式

prototype属性是一个对象,是调用构造函数创建对象时的原型.可以直接给对象原型赋值,这样它的属性或方法可以被对象实例共享

      function Person(){}

      Person.prototype.name = 'zs'
      Person.prototype.age = 20
      Person.prototype.job = 'eng'
      Person.prototype.sayname = function() {
        console.log(name)
      }

1.理解原型

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

2.层级原型

通过对象访问属性时,会现在对象实例开始搜索,没找到才去原型上面找

虽然可以通过实例读取原型对象的值,但是不能通过实例重写.当实例创建同名值时,只会覆盖住原型对象上的属性,当实例的值被删除,原型的值又能被访问到了

3.原型和in操作符

in可以判断属性是否能被实例访问到(在实例上或原型上)

hasOwnProperty()可以判断实例上有无属性

如果要确定一个属性在不在原型上,可以:

      function hasPrototypeProperty(object,name){
        return !object.hasOwnProperty(name) && (name in object)
      }

想要列出所有属性名,可以使用Object.getOwnPropertyNames()

同样的,Object.getOwnPropertySymbols()可以列出所有符号

4.属性枚举顺序

除了Object.getOwnPropertyNames(),Object.getOwnPropertySymbols()和Object.assign(),其他顺序都是不确定的

上三个会先以升序枚举数值键,然后根据插入顺序枚举字符串和符号

8.2.5 对象迭代

Object.values()返回对象值的数组

Object.entries() 返回键值对的数组

这两个静态方法用于把对象内容转化为序列化的,可迭代的格式

其中,非字符串的属性会被转化成字符串,符号属性会被忽略

实例只有指向原型的指针,没有指向原型构造函数的指针

原型的问题:

1.弱化了向构造函数传递初始化参数的能力

2.共享特性,导致不同的实例会拥有相同的属性副本(所以开发中通常不单独使用原型模式)

8.3 继承

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

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

实现原型链涉及以下模式

      function SuperType(){
        this.property = true
      }

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

      function SubType(){
        this.Subproperty = false
      }

      // 继承
      SubType.prototype = new SuperType()
      SubType.prototype.getSubValue = function(){
        return this.property
      }

原型链拓展了前面描述的原型搜索机制,对属性和方法的搜索会一直持续到原型链的末端

1.默认原型

所有引用类型都继承自Object,所以原型链还有一环Object,处于原型链的最顶端

2.原型与继承关系

1.使用instanceof操作符 原型链中如果出现过相应构造函数,返回true

2.使用isPrototypeOf() 原型链中如果出现过相应原型,返回true

3.关于方法

当子类需要增加或覆盖方法时,这些方法必须在原型赋值之后再添加到原型上

※以字面量创建原型方法会破坏之前的原型链,因为相当于重写了原型链

4.原型链的问题

1.原型中包含引用值时,引用值会在原型间共享,所以属性一般会在构造函数上定义而不是在原型上定义

2.子类型在实例化的时候不能给父类型的构造函数传参

8.3.2 盗用构造函数

盗用构造函数又叫对象伪装或经典继承

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

可以用apply()和call()方法以新调用的对象为上下文执行构造函数

      function SuperType(){
        this.property = true
      }

      
      function SubType(){
        SuperType.call(this)
      }

1.传递参数

盗用构造函数可以向父类构造函数传值

      function SuperType(name){
        this.name = name
      }

      
      function SubType(){
        SuperType.call(this,'zs')
        this.age = 20// 实例属性
      }

      let instance = new SubType()
      console.log(instance.name);// zs
      console.log(instance.age);// 20

2.盗用构造函数的问题

必须在构造函数中定义方法,因此函数不能复用

子类也不能访问父类原型上定义的方法

所以盗用构造函数不能单独使用

8.3.3 组合继承

综合了原型链和盗用构造函数,把两者的优点集合了起来

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

      function SuperType(name){
        this.name = name
      }

      SuperType.prototype.sayAge = function (){
        console.log(this.age);
      }

      function SubType(name,age){
        // 继承属性
        SuperType.call(this,name)
        this.age = 20// 实例属性
      }
      
      // 继承方法
      SubType.prototype = new SuperType()
      
      SubType.prototype.sayAge = function (){
        console.log(this.age);
      }

8.3.4 原型式继承

ES5通过      Object.create()  方法把原型式继承的方法概念规范化了

传入两个参数:作为新对象原型的对象,给新对象定义额外属性的对象(可选),在只有一个参数时,和object()等价

      let person = {
        name: 'zs',
        friends: ['A','B','C']
      }

      let d = Object.create(person)
      d.name = 'ls'
      d.friends.push('d')

      let e = Object.create(person)
      e.name = 'ww'
      e.friends.push('e')

      console.log(person.friends); //['A', 'B', 'C', 'd', 'e']

Object.create() 和 Object.defineProperties()的第二个参数一样,每个新增的属性都通过格子的描述符来描述

      let person = {
        name: 'zs',
        friends: ['A','B','C']
      }

      let d = Object.defineProperties(person,{
        name: {
          value: 'ls'
        }
      })
      
      console.log(d.name);// ls

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

8.3.5 寄生式继承

和原型式继承比较接近,创建一个实现继承的函数,以某种方式增强对象,然后返回对像

      function createAnother(original){
        let clone = object(original)// 调用函数创建一个新对象
        clone.satHi = function(){// 以某种方式增强对象
          console.log('hi');
        }
        return clone// 返回
      }

和构造函数模式类似,会导致函数难以复用

8.3.6 寄生式组合继承

组合继承存在的效率问题:父类构造函数始终会调用两次

寄生式组合继承基本思路:不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本

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

寄生式组合继承可以说是最佳的引用类型继承模式

8.4 类

前面讲述了如何使用ES5的特性来模拟类的行为

ES6新引入的class具有正式定义类的能力,但是背后使用的仍然是原型和构造函数的概念

8.4.1 类定义

有两种主要方式:类声明和类表达式

      // 类声明
      class person {}

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

类的构成:

构造函数constructor() 实例方法 获取函数get  静态方法static

8.4.2 类构造函数

方法名constructor会告诉解释器在使用new创建类的新实例时要调用这个函数

1.实例化

使用new调用类的构造函数会进行如下操作:

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

2.在新对象内部的[[Prototype]]指针被赋值为构造函数的prototype属性

3.构造函数内部的this被赋值为这个新对象

4.执行构造函数内部的代码

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

类实例化传入的参数会用作构造函数的参数,如果不需要参数,则类名后面的括号可以省略

类构造函数和构造函数的主要区别:调用类构造函数必须使用new操作符,否则就会作为全局的window对象作为内部对象

ECMAScript中没有正式的类这个类型.类就是一种特殊函数

8.4.3 实例,原型和类成员

1.实例成员

每次使用new调用类标识符,都会执行类构造函数.在函数内部,可以为新创建的实例this添加属性

每个实例都对应一个唯一的成员,这意味着所有成员都不会再原型上共享

2.原型方法与访问器

在类块中定义的方法称为原型方法

在类块中定义的所有内容都会在类的原型上,通过 类名.prototype获取

类方法等同于对象属性

3.静态类方法

可以用static定义静态方法,通常用于执行不特定于实例的操作

      // 存在于不同的实例
        constructor() {
          this.locate = () => console.log('instance', this)
        }
        // 在类的原型
        locate() {
          console.log('prototype', this)
        }
        //在类本身
        static locate() {
          console.log('class', this)
        }
      }

      let p = new Preson()

      p.locate()
      Person.prototype.locate()
      Person.locate()

 

4.非函数原型和类对象

在类外部可以手动添加类的数据成员

      class Person {
        constructor() {
          this.locate = () => console.log('instance', this)
        }
        
      }
      Person.greeting = 'hi'

5.迭代器与生成器方法

类定义语法支持在原型和类本身定义生成器

      class Person {
        constructor() {
          this.nickname = ['a','b','c']
        }
        *[Symbol.iterator](){
          yield *this.nickname.entries()
        }
      }

8.4.4 继承

ES6原生支持了类继承机制,背后仍然时原型链

1.继承基础

使用extends实现单继承

class Bus extends Vehicle {}

2.super()

在派生类中,可以通过super引用它们的原型

注意点:

super只能在派生类的构造函数或静态方法中使用

不能单独引用super关键字,要么调用构造,要么调用静态

调用super()会调用父类构造函数,并且把返回的实例赋值给this

super()的行为类似与调用构造函数,要参数得手动输入

如果没有定义构造函数,则会自动调用super()并传入参数

不能在super之前使用this

如果派生类显式地定义了构造函数,那么必须使用super()或者返回一个对象

3.抽象基类

可以通过new.target实现

      class Vehicle {
        constructor(){
          console.log(new.target);
          if(new.target === Vehicle){
            throw new Error('Vehicle不能被直接取代')
          }
        }
      }

4.继承内置类型

ES6中提供了继承内置类型的顺畅通道,可以方便地扩展内置类型

      class SuperArray extends Array {
        shuffle(){
          // 实现的代码
        }
      }

5.类混入

把不同类的行为集中到一个类

具体方法是定义一组"可嵌套"的函数,每个函数接受一个超类作为对象,把混入类定义为这个函数的子类,并返回这个类.在连缀调用组合函数后,会组成一个超类表达式

      class Vehicle {}

      let FooMixin = (Superclass) => class extends Superclass {
        foo(){
          console.log('foo');
        }
      }

      let BarMixin = (Superclass) => class extends Superclass {
        bar(){
          console.log('bar');
        }
      }

      let BazMixin = (Superclass) => class extends Superclass {
        baz(){
          console.log('baz');
        }
      }

      class Bus extends FooMixin(BarMixin(BazMixin(Vehicle)))

注意:许多JS框架已经抛弃混入转向了组合模式.反映了一个软件设计原则:组合胜过继承

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值