《JavaScript 高级程序设计》第八章 对象、类与面向对象编程 学习记录

90 阅读32分钟
  • 对象:一组属性的无序集合,对象的每个属性或方法都由一个映射到一个值对名称来标识。
  • 想成一个散列表,内容就是一组名/值对,值可以是数据或函数。

1、理解对象

  • 创建自定义对象,创建Object实例。

    let person = new Object()
    person.name = "Feng"
    person.age = 25
    person.sayNam = function() {
      console.log(this.name)
    }
    
  • 对象字面量创建

    let person = {
        name: "Feng",
        age: 25,
        sayName() {
            console.log(this.name);
        }
    }; 
    

1、属性的类型

  • 一些内部特性来描述属性的特征,为js实现引擎规范定义的。
  • 开发者不能直接访问,通常用两个中括号扩起来。

1、数据属性

​ 包含一个保存数据值的位置,值会从这个位置读写。

  • [[Configurable]]

    • 是否可以通过delete删除
    • 是否可以修改它的特性
    • 是否可以把它改为访问器属性
    • 默认直接定义的是true
  • [[Enumerable]]

    • 是否可以通过for-in循环返回
    • 默认直接定义的是true
  • [[Writable]]

    • 是否可以修改属性的值
    • 默认直接定义的是true
  • [[Value]]

    • 包含属性实际的值,读写属性值的位置
    • 默认undefined
  • 修改属性默认特征,必须使用Object.defineProperty()

    • 要给其添加属性的对象
    • 属性的名称
    • 描述符对象 (不设置都是false)
      • configureable (一旦设置为false,不能再改回来)
      • enumerable
      • writable
      • value

2、访问器属性

  • 包含一个获取getter函数和设置setter函数。

  • [[Configurable]]

    • 是否可以通过delete删除并重新定义
    • 是否可以修改它的特性
    • 是否可以把它改为数据属性
    • 默认直接定义的是true
  • [[Enmuerable]]

    • 是否可以通过for-in循环返回
    • 默认直接定义的是true
  • [[Get]]

    • 获取函数,在读取属性的时候调用
    • 默认undefined
  • [[Set]]

    • 设置函数,在写入属性的时候调用
    • 默认undefined
  • 访问器属性不能直接定义,必须使用Object.defineProperty()

      let book = {
        _year: 2017,
        edition: 1
      }
    
      Object.defineProperty(book, "year", {
        get() {
          return this._year
        }
        set(newValue) {
          if (newValue > 2017) {
            this._year = newValue
            this.edition += newValue - 2017
          }
        }
      })
    
    • 只定义获取函数
      • 意味着属性只读
      • 修改会忽略
      • 严格模式报错。
    • 只定义设置函数
      • 意味着属性只写
      • 读取会返回undefined
      • 严格模式报错

2、定义多个属性

  • 同时定义多个属性 Object.defineProperties
    • 要添加或修改属性的对象
    • 描述符对象,其属性和要添加或修改的属性一一对应

3、读取属性的特征

  • 方法 Object.getOwnPropertyDescriptor可以取得制定属性的属性描述符。
    • 属性所在的对象
    • 要取得其描述符的属性名
    • 返回对象
      • 访问属性包含configurable,enumerable,get,set
      • 数据属性包含configurable,enumerable、writabel、value
  • Object.getOwnPropertyDescriptors(),实际在每个自有属性上调用了Object.getOwnPropertyDescriptor(),并在新对象返回

4、合并对象

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

  • 方法Object.assign()

    • 接收目标对象和一个或多个源对象作为参数
    • 将每个源对象中的可枚举(Object.propertyIsEnumerable()返回true)和自有(Object.hasOwnProperty() 返回 true)属性复制到目标对象。
    • 以字符串或符号为键的属性会被复制。
    • 对符合条件的属性,这个方法会使用源对象上的[[Get]]取得属性值,然后使用目标对象上的[[Set]]设置属性的值
    /**
     * 获取函数与设置函数
     */
    let dest, src
    dest = {
     set a(val) {
      console.log(`Invoked dest setter with param ${val}`);
     }
    };
    src = {
     get a() {
      console.log('Invoked src getter');
      return 'foo';
     }
    };
    Object.assign(dest, src);
    // 调用 src 的获取方法
    // 调用 dest 的设置方法并传入参数"foo"
    // 因为这里的设置函数不执行赋值操作
    // 所以实际上并没有把值转移过来
    console.log(dest); // { set a(val) {...} } 
    
  • Object.assign() 实际上对每个源对象执行的是浅复制

  • 如果多个源对象都有相同的属性,则使用最后一个复制的值

  • 从源对象访问属性取得多值,会作为一个静态值复制给目标对象,即不能在两个对象间转移获取函数和设置函数。

    let dest, src
    /**
     * 对象引用
     */
    dest = {};
    src = { a: {} };
    Object.assign(dest, src);
    // 浅复制意味着只会复制对象的引用
    console.log(dest); // { a :{} }
    console.log(dest.a === src.a); // true 
    
  • 如果赋值期间出错,则操作会终止并退出,抛出错误。

  • Object.assign()没有回滚之前赋值概念,可能只会部分复制

    let dest, src;
    /**
     * 错误处理
     */
    dest = {};
    src = {
      a: 'foo',
      get b() {
          // Object.assign()在调用这个获取函数时会抛出错误
          throw new Error();
      }, 
       c: 'bar'
    };
    
    try {
      Object.assign(dest, src);
    } catch(e) {}
    // Object.assign()没办法回滚已经完成的修改
    // 因此在抛出错误之前,目标对象上已经完成的修改会继续存在:
    console.log(dest); // { a: foo } 
    

5、对象标识及相等判定

  • ES6之前,有些特殊情况 === 无能为力

    // 合理情况
    true === 1 // false
    {} === {}  // false
    "2" === 2  // false
    
    // JS引擎中表现不同,但认为相等
    +0 === -0 // true
    +0 === 0 // true
    -0 === 0 // true
    
    // NaN 需要用isNaN()
    NaN === NaN // false
    
  • ES6新增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
    
  • 检查超过两个值,递归利用相等性

    function recursiveCheckEqual(x, ...rest){
      return Object.is(x, rest[0]) &&
        (rest.length < 2 || recursiveCheckEqual(...rest))
    }
    

6、增强的对象语法

  • 为定义和操作对象新增了很多语法糖特征,提升了处理对象的方便程度
  • 同样适用于ES6中的类

1、属性值简写

  • 属性名和变量名一样,只要写变量名。

    let name = "Feng"
    let person = {
      name
    }
    

2、可计算属性

  • 在此之前,如果想要变量的值作为属性,必须先声明对象,然后使用中括号语法添加属性。

  • 有了可计算属性,可以在对象字面量中完成动态属性赋值

    const nameKey = 'name';
    const ageKey = 'age'; 
    const jobKey = 'job';
    
    // old
    let person = {};
    person[nameKey] = 'Matt';
    person[ageKey] = 27;
    person[jobKey] = 'Software engineer'; 
    
    // new
    let person = {
      [nameKey]: "Matt",
      [ageKey]: 27,
      [jobKey]: "Software enegineer"
    }
    
  • 可计算属性本身可以是复杂表达式,在实例化时再求值

    const nameKey = 'name';
    const ageKey = 'age'; 
    const jobKey = 'job';
    
    let uniqueToken = 0;
    
    function getUniqueKey(key) {
      return `${key}_${uniqueToken++}`;
    }
    
    let person = {
      [getUniqueKey(nameKey)]: 'Matt',
      [getUniqueKey(ageKey)]: 27,
      [getUniqueKey(jobKey)]: 'Software engineer'
    };
    console.log(person); // { name_0: 'Matt', age_1: 27, job_2: 'Software engineer' }
    
  • 可计算属性表达式抛出错误会中断对象创建,之前的计算不能回滚

3、简写方法名

  • 放弃给函数表达式命名

    // old
    let person = {
      sayName: function(name) {
          console.log(`My name is ${name}`);
      }
    };
    
    // new
    let person = {
      sayName(name) {
          console.log(`My name is ${name}`);
      }
    }; 
    
  • 对set,get同样适用

  • 可以于计算属性键兼容

    const methodKey = 'sayName';
    let person = {
      [methodKey](name) {
          console.log(`My name is ${name}`);
      }
    }
    

7、对象解构

  • 对象解构,可以在一条语句中使用嵌套数据实现一个或多个赋值操作

  • 使用与对象匹配的解构来实现对象属性赋值

    let person = {
     name: 'Matt',
     age: 27
    }; 
    
    // 不使用对象解构
    let personName = person.name,
     personAge = person.age;
    console.log(personName); // Matt
    console.log(personAge); // 27
    
    // 使用对象解构
    let { name: personName, age: personAge } = person;
    console.log(personName); // Matt
    console.log(personAge); // 27
    
  • 如果想让变量直接使用属性的名称,那么可以使用简写语法

    let { name, age } = person;
    console.log(name); // Matt
    console.log(age); // 27
    
  • 解构赋值不一定与对象属性匹配,可以忽略某些属性,如果引用属性不存在则为undefined

    let {name, job} = person
    console.log(name); // Matt
    console.log(job); // undefined
    
  • 解构时可以定义默认值

    let {name, job = 'teacher'} = person
    console.log(name); // Matt
    console.log(job); // teacher
    
  • 解构在内部调用ToObject() 把源数据结构转为对象。意味着原始值会被当成对象,也意味着null和undefined不能被解构

    let { length } = 'foobar'
    length // 6
    
    let {constructor: c} = 4
    c === Number // true
    
    let { _ } = null // TypeError
    let { _ } = undefined // TypeError
    
  • 不要求变量必须在解构表达式中声明,但若给实现声明变量赋值,则赋值表达式必须包含在一对括号里

    let personName, personAge
    let person = {
      name: 'Matt',
      age: 27
    }; 
    ({name: personName, age: personAge} = person)
    

1、嵌套解构

let person = {
    name: 'Matt',
    age: 27,
    job: {
        title: 'Software engineer'
    }
};
  • 解构对与引用嵌套的属性或赋值目标没有限制,可用来赋值对象属性

    let personCopy = {};
    ({
      name: personCopy.name,
      age: personCopy.age,
      job: personCopy.job
    } = person);
    
    // 因为一个对象的引用被赋值给 personCopy,所以修改
    // person.job 对象的属性也会影响 personCopy
    person.job.title = 'Hacker'
    console.log(person);
    // { name: 'Matt', age: 27, job: { title: 'Hacker' } }
    console.log(personCopy);
    // { name: 'Matt', age: 27, job: { title: 'Hacker' } }
    
  • 解构赋值也可以用嵌套结构

    // 声明title变量并将person.job.title的值赋给它
    let {job: {title}} = person
    console.log(title) // Software engineer
    
  • 在外层属性没有定义的情况下不能使用嵌套解构,无论源对象还是目标对象

2、部分解构

  • 涉及多个属性的解构赋值是一个输出无关的顺序化操作

  • 如果开始赋值成功而后面出错,则只会完成一部分

    let person = {
      name: 'Matt',
      age: 27
    };
    let personName, personBar, personAge;
    try {
    // person.foo 是 undefined,因此会抛出错误
      ({
          name: personName,
          foo: { bar: personBar }, 
          age: personAge
       } = person);
    } catch(e) {}
    console.log(personName, personBar, personAge);
    // Matt, undefined, undefined 
    

3、参数上下文匹配

  • 函数的参数列表也可以解构赋值

  • 不会影响arguments对象,但可以在函数签名中声明在函数体内使用局部变量。

    let person = {
      name: 'Matt',
      age: 27
    };
    function printPerson(foo, {name, age}, bar) {
      console.log(arguments);
      console.log(name, age);
    }
    function printPerson2(foo, {name: personName, age: personAge}, bar) {
      console.log(arguments);
      console.log(personName, personAge);
    }
    printPerson('1st', person, '2nd');
    // ['1st', { name: 'Matt', age: 27 }, '2nd']
    // 'Matt', 27
    printPerson2('1st', person, '2nd');
    // ['1st', { name: 'Matt', age: 27 }, '2nd']
    // 'Matt', 27
    

2、创建对象

  • 创建具有同样接口的多个对象需要重复代码

1、概述

  • ES6开始正式支持类和继承,ES5可以通过运用原型链继承模拟

2、工厂模式

  • 用于抽象创建特定对象的过程

    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("Jack", 14, "student")
    let person2 = createPerson("Mick", 32, "teacher")
    
    • 没有解决对象标识问题(新创建的对象是什么类型)

3、构造函数模式

  • 自定义构造函数,以函数的形式为自己的对象类型定义属性方法

    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("Jack", 14, "student")
    let person2 = new Person("Mick", 32, "teacher")
    person1.sayName() // Jack
    person1.sayName() // Mick
    
    • Person() 构造函数替代了createPerson() 工厂函数

    • Person()内部代码和createPerson()基本一致 除了

      • 没有显示创建对象
      • 属性和方法赋值给了this
      • 没有return
    • Person() 构造函数首字母大写

    • 创建Person实例要用new操作符,实际执行了

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

      person1.constructor === Person // true
      person2.constructor === Person // true
      
    • constructor是标识对象类型的,但instanceof是更可靠方式

      person1 instanceof Person //true
      person1 instanceof Object //true
      
  • 构造函数不一定要写成函数声明的模式

    let Person = function(name, age ,job) {
      ...
    }
    ...
    
  • 实例化时没如果不想传参数,那么构造函数后面括号可以省略

    let person2 = new Person
    

1、构造函数也是函数

  • 唯一区别就是调用方式不同。任何函数只要使用new操作符就是构造函数,不使用就是普通函数。

    // 作为构造函数
    let person = new Person("Nicholas", 29, "Software Engineer");
    person.sayName(); // "Nicholas"
    
    // 作为函数调用
    Person("Greg", 27, "Doctor"); // 添加到 window 对象
    window.sayName(); // "Greg"
    
    // 在另一个对象的作用域中调用
    let o = new Object();
    Person.call(o, "Kristen", 25, "Nurse");
    o.sayName(); // "Kristen" 
    
    • 在调用一个函数没有明确设置this的情况下(即没有作为对象的方法调用,或者没有使用call/apply调用),this始终指向Global对象(浏览器window对象)。
    • 通过call() 调用函数将特定对象制定为作用域。

2、构造函数的问题

  • 定义的方法会在每个实例上都创建一遍,即不同实例的相同方法不是同一个Function实例。每次定义函数的时候都会初始化一个对象,实际相当于这样的:

    function Person(name, age, job) {
      this.name = name
      this.age = age 
      this.job = job
      this.sayName = 
        new Function("console.log(this.name)")
    }
    
    • 不同实例上的函数虽然同名但并不相等
    person1.sayName == person2.sayName // false
    
  • 可以通过把函数定义转移到构造函数外部解决这个问题

    function Person(name, age, job) {
      this.name = name
      this.age = age
      this.job = job
      this.sayName = sayName
    }
    
    function sayName() { 
      console.log()
    }
    
    • 缺点:全局作用域混乱了,可以通过原型模式解决

4、原型模式

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

  • prototype这个对象就是通过调用构造函数创建的对象的原型。

  • 在prototype上面定义的属性和方法可以被对象实例共享。

  • 原来在构造函数上直接赋值给对象实例等值,可以直接赋值给它们的原型prototype

    function Person() {}
    // 或 let Person = function() {}
    Person.prototype.name = "Nike"
    Person.prototype.age = 18
    Person.prototype.job = "student"
    Person.prototype.sayName = function() {
      console.log(this.name)
    }
    
    let person1 = new Person()
    let person2 = new Person()
    person1.sayName() // Nike
    person2.sayName() // Nike
    person1.sayName === person2.sayName
    

1、理解原型

  • 只要创建一个函数,就会按特定规则为该函数创建prototype属性(指向原型对象)。

  • 所有原型对象自动获得一个名为constructor的属性,指回与之关联的构造函数。

    Person.prototype.constructor === Person // true
    
  • 因构造函数而异,可以给原型对象添加其他属性方法

  • 定义构造函数的时候,原型对象默认只会获得constructor属性,其他方法都继承自Object

  • 每次调用构造函数创建新实例,这个实例内部[[Prototype]]指针会被赋值为构造函数的原型对象,这个就是__proto__,通过这个属性可以访问对象的原型

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

    person1.__proto__ === Person.prototype
    
    function Person() {}
    
    // 声明之后,构造函数就有了一个与之关联的原型对象
    typeof Person.prototype // object
    Person.prototype
    //{
    //	constructor: f Person(),
    //  __proto__: Object
    //}
    
    // 构造函数的prototype属性引用其原型对象
    // 这个原型对象也有一个constructor属性引用这个构造函数
    // 所以两者循环引用
    Person.prototype.constructor === Person // true
    
    // 正常原型链都会终止于Object的原型对象
    // Object原型的原型是null
    Person.prototype.__proto__ === Object.prototype
    Person.prototype.__proto__.constructor === Object
    Person.prototype.__proto__.__proto__ === null
    
    Person.prototype.__proto__ 
    //{
    //	constructor: f Object(),
    //	toString: ...
    //	hasOwnProperty: ...
    //	isPrototypeOf: ...
    //	...
    //}
    
    let person1 = new Person()
    let perosn2 = new Person()
    
    person1 !== Person
    person1 !== Person.prototype
    Person.prototype !== Person
    
    // 实例可以通过__proto__ 链接到原型对象
    person1.__proto__ === Person.prototype
    person1.__proto__.constructor === Person
    
    // 同一个构造函数的两个实例共享一个原型对象
    person1.__proto__ === person2.__proto__
    
    person1 instanceof Person
    person1 instanceof Object
    Person.prototype instanceof Object
    

image-20210530172548481.png

  • isPrototypeOf() 确定两个对象之间的__proto__关系

    Person.prototype.isPrototypeOf(person1) // true
    Person.prototype.isPrototypeOf(person2) // true
    // 即 person1.__proto__ === Person.prototype
    // 即 person2.__proto__ === Person.prototype
    
  • Object.getPrototypeOf() 返回参数的内部特性[[Prototype]]

    Object.getPrototypeOf(person1) === Person.prototype
    Object.getPrototypeOf(person1).name // Nike
    
  • Object.setPrototypeOf可以向私有特性[[Prototype]]写入一个新值。可以重写对象原型继承关系

    let biped = {
      numLegs: 2
    }
    let person = {
      name: "Matt"
    }
    Object.setPrototypeOf(person, biped)
    person.name // "Matt"
    person.numLegs // 2
    Object.getPrototypeOf(person) === biped
    
    • 严重影响性能,严重影响继承关系,避免使用
  • Object.create()创建新对象,同时指定原型

    let biped = {
      numLegs: 2
    }
    let person = Object.create(biped)
    person.name = "Matt"
    
    person.name // "Matt"
    person.numLegs // 2
    Object.getPrototypeOf(person) === biped
    

2、原型层级

  • 通过对象访问属性,会按照这个属性的名称开始搜索。

  • 从这个实例本身开始搜索,没找到的时候会沿着指针进入原型对象

  • constructor属性只存在于原型对象中,所以通过实例对象也可以访问到的。

  • 虽然可以通过实例读取到原型对象的值,但不可能通过实例去重写这些值。如果添加一个与原型对象同名属性,那么会在实例上创建这个属性,这个属性会遮住原型对象上的属性

    function Person() {}
    
    Person.prototype.name = "Nicholas"
    Person.prototype.age = 29
    Person.prototype.job = "Software Engineer"
    Person.prototype.sayName = function() {
      console.log(this.name)
    }
    
    let person1 = new Person()
    let person2 = new Person()
    
    person1.name = "Grace"
    perosn1.name // "Grace" 来自实例
    person2.name // "Nicholas" 来自原型
    
    // 即使把实例上这个属性设置为null,也无法恢复它和原型的联系
    // 不过可以通过delete操作完全删除实例上的这个属性
    delete person1.name
    person1.name // "Nicholas" 来自原型
    
  • hasOwnProperty()方法用于确定某个属性是在实例上还是在原型对象上。该方法继承自Object的,会在属性存在于调用它的对象实例上返回true。

    let person1 = new Person()
    
    person1.hasOwnProperty("name") // false
    person1.name = "Franck" 
    person1.hasOwnProperty("name") // true
    delete person1.name
    person1.hasOwnProperty("name") // false
    
  • Object.getOwnPropertyDescriptor只对实例属性有效,若想获取原型属性的描述符,必须直接在原型对象上调用。

3、原型和in操作符

  • in操作符

    • for-in 循环
    • 单独使用
      • 可以通过对象访问指定属性时返回true
      • 无论在实例上还是在原型上
    let person1 = new Person()
    
    person1.hasOwnProperty("name") // false
    "name" in person1 // true
    
    person1.name = "Greg"
    person1.hasOwnProperty("name") // true
    "name" in person1 // true
    
    delete person1.name
    person1.hasOwnProperty("name") // false
    "name" in person1 // true
    
  • 确定某个属性是否仅存在于原型上

    function hasOwnPrototypeProperty(object, name) {
      return !object.hasOwnProperty(name) && (name in object)
    }
    
    let person = new Person()
    hasOwnPrototypeProperty(person, "name") // true
    person.name = "Greg"
    hasOwnPrototypeProperty(person, "name") // false
    
  • for-in循环使用in,可以通过对象访问且可以被枚举的属性都会返回,包括实例属性和原型属性。遮蔽原型中不可枚举([[Enmuerable]]特征被设置为false)属性的实例属性也会在for-in循环返回。

  • 通过Object.keys()方法获得对象上所有可枚举的实例属性

    • 接收一个对象作为参数
    • 返回包含该对象所有可枚举属性名称的字符串数组
    Object.keys(Person.prototype)
    // ["name","age","job","sayName"]
    let p1 = new Person()
    p1.name = "Rob"
    p1.age = 31
    Object.keys(p1)
    // ["name", "age"]
    
  • Object.getOwnPropertyNames所有实例属性,无论是否可枚举使用

    Object.getOwnPropertyNames(Person.prototype)
    // ["constructor", "name", "age", "job", "sayName"]
    
  • Object.getOwnPropertySymbols仅针对Symbol符号

    let k1 = Symbol('k1')
    let k2 = Symbol('k2')
    let o = {
      [k1]: 'k1',
      [k2]: 'k2'
    }
    Object.getOwnPropertySymbols(o) 
    // [Symbol(k1),Symbol(k2)]
    

4、属性枚举循环

  • for-in

    • 枚举顺序不确定
    • 返回可以被枚举的实例属性原型属性都会
  • Object.keys()

    • 枚举顺序不确定
    • 返回可以被枚举的实例属性
  • Object.getOwnPropertyNames()

    • 顺序确定
      • 先以升序枚举数值键
      • 然后以插入顺序枚举字符串和符号键
    • 列出所有非符号实例属性,无论是否可枚举使用
  • Object.getOwnPropertySymbols()

    • 顺序确定
      • 先以升序枚举数值键
      • 然后以插入顺序枚举字符串和符号键
    • 列出所有符号实例属性,无论是否可枚举使用
  • Object.assign()

    • 顺序确定
      • 先以升序枚举数值键
      • 然后以插入顺序枚举字符串和符号键
    let k1 = Symbol('k1'),
      k2 = Symbol('k2');
    let o = {
      1: 1,
      first: 'first',
      [k1]: 'sym2',
      second: 'second',
      0: 0
    };
    o[k2] = 'sym2';
    o[3] = 3;
    o.third = 'third';
    o[2] = 2;
    console.log(Object.getOwnPropertyNames(o));
    // ["0", "1", "2", "3", "first", "second", "third"]
    console.log(Object.getOwnPropertySymbols(o));
    // [Symbol(k1), Symbol(k2)] 
    

5、对象迭代

  • Object.values()

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

    • 返回键/值对的数组
    const o = {
      foo: "bar",
      baz: 1,
      qux: {}
    }
    Object.values(o) 
    // ["bar", 1, {}]
    Object.entries(o) 
    // [["foo","bar"],["baz",1],["qux",{}]]
    
  • 非字符串属性会被转换为字符串输出,执行浅复制

    const o = {
      qux: {}
    }
    Object.values(o)[0] === o.qux
    Object.entries(o)[0][1] === o.qux
    
  • 符号属性会被忽略

    const sym = Symbol()
    const o = {
      [sym]: "foo"
    }
    Object.values(o) // []
    Object.entries(o) // []
    

1、其他原型方法

  • 字面量方法重写原型

    function Person() {}
    Person.prototype = {
      name: "Nicholas",
      age: 29,
      job: "Software Engineer",
      sayName() {
          console.log(this.name);
      } 
    }
    
    • 问题 重写后Person.prototype的constructor属性不能指向Person

      let friend = new Person()
      friend instance of Object // true
      friend instance of Person // true
      friend.constructor === Person //false
      friend.constructor === Object //true
      
    • 这样重写,[[Enmuerable]]为true

      function Person() {}
      Person.prototype = {
          constructor: Person,
          name: "Nicholas",
          age: 29,
          job: "Software Engineer",
          sayName() {
              console.log(this.name);
          } 
      }
      
      Object.defineProperty(Person.prptotype, "constructor",{
        enumerable: false,
        value: Person
      })
      

2、原型的动态性

  • 从原型上搜索值是动态的,即使实例在原型修改前已经存在,任何时候对原型对象的修改也会在实例上反应出来。

    let friend = new Person()
    Person.prototype.sayHi = function() {
      console.log('hi')
    }
    friend.sayHi() // hi
    
    • 虽然friend实例在添加方法后面创建的,但仍可访问。

      • 实例和原型之间松散的联系,所以会搜索原型对象。
      • 实例和原型之间就是简单的指针而不是保存副本,所以可以找到这个函数。
    • 但如果是重写整个原型

      • 实例的[[Prototype]]指针是在调用构造函数的时候自动赋值的 ,即使把原型修改成不同的对象也不会变
      • 重写整个原型会切断最初原型与构造函数的联系。
      • 实例仍然引用最初原型,实例只有指向原型的指针,而没有指向构造函数的指针
      function Person() {}
      let friend = new Person();
      Person.prototype = {
      	constructor: Person,
      	name: "Nicholas",
      	age: 29,
      	job: "Software Engineer",
      	sayName() {
      		console.log(this.name);
      	}
      };
      friend.sayName(); // 错误
      

image-20210530211139652.png

3、原生对象原型

  • 原型模式也是实现所有原生引用类型的模式

  • 所有原生引用类型(包括Object,Array,String等)都在原型上定义了实例方法

    typeof Array.prototype.sort // function
    typeof String.prototype.substring // function
    
  • 可以给原生类型定义新方法

  • 不推荐在产品环境修改原生对象原型,可能造成意外。

  • 推荐创建一个自定义的类继承原生类型

4、原型的问题

  • 弱化了向构造函数传递初始化参数的能力,导致所有实例默认取得相同的属性值。

  • 共享特征,原型上所有属性都是在实例间共享的,对于包含引用值的属性会有问题

    function Person() {}
    Person.prototype = {
        constructor: Person,
        name: "Nicholas",
        age: 29,
        job: "Software Engineer",
        friends: ["Shelby", "Court"],
        sayName() {
            console.log(this.name);
        }
    };
    let person1 = new Person();
    let person2 = new Person();
    person1.friends.push("Van");
    person1.friends // ["Shelby", "Court", "Van"]
    person2.friends // ["Shelby", "Court", "Van"]
    person1.friends === person2.friends // true
    
    • 由于friends存在Perosn.prototype而非person1上,新加的内容也会在person2.friends上反映出来
    • 影响了自己属性副本的独立性。这有是不单独使用原型模式原因

3、继承

  • 其他语言:
    • 接口继承(只继承方法签名,JS不可能)
    • 实现继承(继承实际的方法)

1、原型链

  • 主要的继承方式,通过原型链继承多个引用类型的属性和方法

  • 基本构想

    • 每个构造函数都有一个原型对象
    • 原型有一个属性指向构造函数
    • 实例有一个内部指针指向原型
    • 原型又是另一个类型的实例
      • 原型本身有一个内部指针指向另一个原型
      • 另一个原型也有一个属性指向另一个构造函数
  • 实现原型链:

    function SuperType() {
      this.property = true
    }
    
    SuperType.prototype.getSuperValue = function() {
      return this.property
    }
    
    function SubType() {
      this.subproperty = false
    }
    
    // 继承SuperType
    SubType.prototype = new SuperType()
    
    SubType.prototype.getSubValue = function() {
      return this.subproperty
    }
    
    let instance = new SubType()
    instance.getSuperValue() // true
    
    • SubType 通过 创建 SuperType的实例并将其赋值给自己的原型SubType.prototype实现了对SuperType的继承
    • 重写了SubType最初的原型替换为SuperType实例
    • SuperType实例可以访问的所有属性和方法也存在于SubType.prototype
    • 又给SubType.prototype即SuperType的实例添加新方法
    • 创建了SubType的实例instance
    • instance.getSuperValue() 搜索了三次 才找到这个方法。

image-20210531234730135.png

1、默认原型

  • 默认情况下,所有引用类型都继承自Object,任何函数默认原型都是一个Object实例
  • 实例有一个内部指针指向Object.prototype, 因此自定义类型能继承包含toString()、valueOf() 在内的所有默认方法。

2、原型与继承的关系

  • instanceof

    • 如果一个实例的原型链中出现过相应的构造函数则返回true
    instance instanceof Object
    instance instanceof SuperType
    instance instanceof SubType
    
  • isPrototypeOf()

    • 原型链中每个原型都可以调用这个方法,只要原型链中包含这个属性就返回true
    Object.prototype.isPrototypeOf(instance)
    SuperType.prototype.isPrototypeOf(instance)
    SubType.prototype.isPrototypeOf(instance)
    

3、关于方法

  • 子类需要覆盖父类方法或者增加父类方法

    function SuperType(){
      this.property = true
    }
    
    SuperType.prototype.getSuperValue = function() {
      return this.property
    }
    
    function SubType() {
      this.subproperty = false
    }
    
    // 继承SuperType
    SubType.prototype = new SuperType()
    // 新方法
    SubType.prototype.getSubValue = function() {
      return this.subproperty
    }
    // 覆盖已有方法
    SubType.prototype.getSuperValue = function() {
      return false
    }
    
  • 对象字面量方式创建原型方法会破坏之前的原型链,相当于重写了原型链

    // 继承SuperType
    SubType.prototype = new SuperType()
    // 新方法
    SubType.prototype = {
      getSubValue() {
        return this.subproperty
      },
      someOtherMethod() {
        return false
      }
    }
    let instance = new SubType()
    instance.getSuperValue() // 报错!!
    

4、原型链问题

  • 原型中包含引用值的时候会在所有实例共享,所以属性经常会在构造函数定义而不会定义在原型上
    • 使用原型实现继承时,原型实际上变成了另一个类型的实例,即原先的实例属性摇身一变成了原型属性。
function SuperType() {
 this.colors = ["red", "blue", "green"];
}
function SubType() {}
// 继承 SuperType
SubType.prototype = new SuperType();

let instance1 = new SubType();
instance1.colors.push("black");
instance1.colors // "red,blue,green,black"

let instance2 = new SubType();
instance2.colors // "red,blue,green,black" 
  • 子类型在实例化时不能给父类型构造函数传参
    • 无法在不影响所有对象实例的情况下把参数传给父类的构造函数。同时原型中包含引用值问题,则原型链不会单独使用

2、盗用构造函数

  • 目的是解决原型包含引用值导致的继承问题。

  • 又称“对象伪装”或“经典继承”

  • 思路:

    • 子类构造函数中调用父类构造函数
    • 函数就是在特定上下文中执行代码的简单对象,用apply() 和 call() 方法以新创建的对象为上下文执行构造函数
    function SuperType() {
     this.colors = ["red", "blue", "green"];
    }
    function SubType() {
      SuperType.call(this)
    }
    
    let instance1 = new SubType();
    instance1.colors.push("black");
    instance1.colors // "red,blue,green,black"
    
    let instance2 = new SubType();
    instance2.colors // "red,blue,green" 
    
    • 通过使用call或apply,SuperType在构造函数为SubType的实例创建新对象的上下文执行了。

1、传递参数

function SuperType(name) {
 this.name = name
}
function SubType() {
  // 继承并传参
  SuperType.call(this, "Feng")
  // 实例属性
  this.age = 29
}
let instance = new SubType()
instance.name // "Feng"
instance.age // 29

2、盗用构造函数问题

  • 必须在构造函数中定义方法,因此函数不能重用
  • 子类不能访问父类原型上的方法,所有类型只能使用构造函数模式。

3、组合继承

  • 组合继承(也称伪经典继承)

  • 综合了原型链和盗用构造函数,集中了两者优点。

  • 使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性。

    function SuperType(name) {
      this.name = name
      this.colors = ["red", "blue", "green"]
    }
    
    SuperType.prototype.sayName = function() {
      console.log(this.name)
    }
    
    function SubType(name, age) {
      SuperType.call(this, name)
      this.age = age
    }
    
    SubType.prototype = new SuperType()
    
    SubType.prototype.sayAge = function() {
      console.log(this.age)
    }
    
    let instance1 = new SubType("Feng", 25)
    instance1.colors.push("black")
    instance1.colors //["red","blue","green","black"]
    instance1.sayName() // "Feng"
    instacn1.sayAge() // 25
    
    let instance2 = new SubType("Zhu", 23)
    instance1.colors //["red","blue","green"]
    instance1.sayName() // "Zhu"
    instacn1.sayAge() // 23
    
    • SuperType 构造函数定义了两个属性,原型上定义了一个方法
    • SubType构造函数调用了SuperType构造函数,又定义了自己的属性
    • SubType.prototype也被赋值为SuperType实例
    • 这个原型上添加了新方法,创建的实例有自己的属性,同时共享相同方法
  • 弥补了原型链和盗用构造函数不足,使用最多。

  • 同时保留了instanceof操作符和isPrototypeOf() 方法识别合成对象能力

4、原型式继承

  • 即使不自定义类型也可通过原型实现对象间的信息共享

    function object(o) {
      function F() {}
      F.prototype = o
      return new F()
    }
    
    • 创建一个临时构造函数,将传入的对象复制给这个构造函数原型
    • 返回这个临时类型的一个实例。
    • 本质上是进行了一次浅复制
    let person = {
    	name: "Nicholas",
    	friends: ["Shelby", "Court", "Van"]
    };
    let anotherPerson = object(person);
    anotherPerson.name = "Greg";
    anotherPerson.friends.push("Rob");
    
    let yetAnotherPerson = object(person);
    yetAnotherPerson.name = "Linda";
    yetAnotherPerson.friends.push("Barbie");
    
    console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie" 
    
  • 在一个对象的基础上创建一个新对象,需要把这个对象先传给object,然后再对返回的对象进行适当修改。

  • 原型式继承概念规范化 --> Object.create()

    • 参数一,作为新对象原型的对象
    • 参数二,给新对象定义额外属性的对象(可选)
    • 只有一个参数等同于object方法
    • 第二个参数与Object.definePrototype()第二个参数一样
      • 每个新增属性都通过各自的描述符描述
      • 这种方式添加的属性会遮蔽原型对象上的同名属性
  • 非常适合不需要单独创建构造函数,但仍需在对象间共享信息的场合

  • 属性中包含的引用值始终会在相关对象间共享,跟使用原型模式一样

5、寄生式继承

  • 创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。

    function createAnother(original) {
      let clone = object(original) // 通过调研函数创建一个对象
      clone.sayHi = function() {  // 以某种方式增强这个对象
        console.log('hi')
      }
      return clone // 返回这个对象
    }
    
  • 适合主要关注对象而不在乎类型和构造函数场景

  • 通过寄生式继承给对象添加函数导致函数难以重用,类似构造函数模式。

6、寄生式组合继承

  • 组合继承存在效率问题

    • 父类构造函数始终会被调用两次

      • 创建子类原型时
      • 子类构造函数中调用
    • 本质上子类原型最终要包含超类对象的所有实例属性,子类构造函数只要在执行时重写自己原型就行了。

      function SuperType(name) {
        this.name = name
        this.color = ['red','green','blue']
      }
      
      SuperType.prototype.sayName = function() {
        console.log(this.name)
      }
      
      function SubType(name, age) {
        SuperType.call(this, name)
        this.age = age
      }
      
      SubType.prototype = new SuperType()
      SubType.prototype.constructor = SubType
      
      SubType.prototype.sayAge = function() {
        console.log(this.age)
      }
      
  • 寄生组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。

  • 基本思路

    • 不通过调用父类构造函数给子类原型赋值,而是取得父类原型的副本
    • 使用寄生式继承来继承父类原型,将返回的新对象赋值给子类原型
    function object(o) {
      function F() {}
      F.prototype = o
      return new F();
    }
    
    function inheritPrototype(subType, superType) {
     let prototype = object(superType.prototype) // 创建对象
      prototype.constructor = subType // 增强对象
      subType.prototype = prototype // 赋值对象
    }
    
    • 实现了寄生式组合继承的核心逻辑

      • 先创建父类原型的副本
      • 给返回的prototype对象设置constructor属性,解决重写原型导致默认constructor丢失问题。
      • 将新创建的对象赋值给子类型的原型。
      function SuperType(name) {
        this.name = name
        this.colors = ['red', 'blue', 'green']
      }
      
      SuperType.prototype.sayName = function() {
       console.log(this.name)
      }
      
      function SubType(name, age) {
        SuperType.call(this, name)
        this.age = age
      }
      inheritPrototype(SubType, SuperType)
      SubType.prototype.sayAge = function() {
        console.log(this.age)
      }
      
      • 只调用了一次SuperType构造函数,避免了SubType.prototype上不必要也用不到的属性
      • 效率更高,原型链保持不变,instanceof操作符和isPrototypeOf() 方法正常有效,最佳模式。

4、类

  • 实际上是语法糖结构,背后仍然是原型和构造函数

1、类定义

  • 类声明 class Person{}

  • 类表达式 const Animal = class {}

  • 与函数的区别

    • 函数声明可以提升,但类定义不能

      Person // 报错
      class Person {}
      
      Dog // undefined
      var Dog = class {}
      
      Animal // Animal() {}
      function Animal() {}
      
      Pig // undefined
      var Pig = function() {}
      
    • 函数受函数作用域限制,类受块作用域限制

      {
        function FunDeclaration(){}
        class ClassDeclaration{}
      }
      FunDeclaration // FunDeclaration() {}
      ClassDeclaration // 报错
      
  • 类的构成

    • 包含构造函数方法、实例方法、获取函数、设置函数和静态类方法

    • 类名首字母大写以区别于创建的实例

    • 类表达式名称可选,表达式复制给变量后可以通过name属性获得类表达式名称字符串,但不能在类表达式作用域外访问这个标志符

      let Person = class PersonName {
        print() {
          console.log(Person.name, PersonName.name)
        }
      }
      let p = new person()
      p.print() // PersonName PersonName
      Person.name // PersonName
      PersonName // 报错
      

2、类构造函数

  • 不定义相当于空的构造函数

1、实例化

  • 使用new操作符实例化Person的操作等于使用new调用其构造函数

    1. 在内存中创建一个新对象
    2. 这个对象内部 [[Prototype]]指针被赋值为构造函数的prototype属性
    3. 构造函数内部的this被赋值为这个新对象(this指向新对象)
    4. 执行构造函数内部代码(给新对象添加属性)
    5. 如果构造函数返回非空对象则返回该对象否则返回新创建的对象
  • 不传参数的时候,括号可选

  • 默认返回this对象,如果返回其他对象,这个对象不会通过instanceof操作符检测出跟类有关联,因为这个类的原型指针没修改

    class Person {
      constructor(override) {
          this.foo = 'foo'
          if(override) {
              return {
                bar: 'bar'
              }
          }
      }
    }
    
    let p1 = new Person()
    let p2 = new Person(true)
    p1 // Perosn{foo: 'foo'}
    p1 instanceof Person // true
    p2 // {bar: 'bar'}
    p2 instanceof Person //false
    
  • 类构造函数必须使用new操作符,普通构造函数不用new的时候会以全局this作为内部对象

    function Person() {}
    class Animal {}
    // window 作为this来构建实例
    let p = Person()
    
    let a = Animal() //报错
    
  • 没有特别之处,实例化后就是普通的实例方法,但作为类构造函数,仍要用new调用,实例化之后可以在实例上引用它

    class Person {}
    
    // 使用类创建一个新实例
    let p1 = new Person()
    
    p1.constructor() // 报错
    
    // 使用对类构造函数的引用创建一个新实例
    let p2 = new p1.constructor()
    

2、把类当成特殊函数

  • 可以用typeof操作符检测是函数

    class Person {}
    typeof Person // functon
    
  • 类标志符有prototype属性,而这个原型也有一个constructor属性指向类自身

    class Person {}
    Person.prototype // {constructor: f()}
    Person === Person.prototype.constructor
    
  • 可以使用instanceof操作符检查构造函数原型是否存在于实例原型链中

    class Person {}
    let p = new Person()
    p instanceof Person // true
    
  • 类的上下文中,类本身在使用new调用时就会被当成构造函数

    • 类中定义的constructor不会被当作构造函数,对其使用instanceof返回false。
    • 如果在创建实例时直接将类构造函数当成普通构造函数来使用,那么instanceof操作符的返回值会反转
    class Person{}
    let p1 = new Person()
    p1.constructor === Person //true[实际是__proto__下]
    p1 instanceof Person // true
    p1 instanceof Person.constructor // false
    
    let p2 = new Person.conctructor()
    p2.constructor === Person // false
    p2 instanceof Person // false
    p2 instanceof Person.constructor // true
    
  • 可以像其他对象或函数引用一样把类作为参数传递

  • 与立即调用函数表达式类似,类可以立即实例化

    let p = new Class Foo {
      constructor(x) {
        console.log(x)
      }
    }('bar') // bar
    console.log(p) // Foo {}
    

3、实例、原型和类成员

1、实例成员
  • 在构造函数内部,可以为新创建的实例(this)添加“自有”属性。

  • 添加什么样的属性没有限制

  • 构造函数执行完毕后,仍可以给实例添加新成员

  • 每个实例都对应一个唯一的成员对象,所有成员都不会在原型上共享。

    class Person {
      constructor() {
        this.name = new String('Jack')
        this.sayName = ()=> console.log(this.name)
        this.nicknames = ['Jake', 'J-Dog']
      }
    }
    
    let p1 = new Person()
    let p2 = new Person()
    
    p1.sayName() //Jack
    p2.sayName() //Jack
    
    p1.name === p2.name //false
    p1.sayName === p2.sayName //false
    p1.nicknames === p2.nicknames //false
    
    p1.name = p1.nicknames[0]
    p2.name = p2.nicknames[1]
    p1.sayName() // Jake
    p2.sayName() // J-Dog
    
2、原型方法与访问器
  • 为了在实例间共享方法,类定义语法在类块内定义的方法作为原型方法

    class Person {
      constructor() {
        // 添加this的所有内容都会存在于不同实例上
        this.locate = () => console.log('instance')
      }
      locate() {
        console.log('prototype')
      }
    }
    
    let p = new Person()
    p.locate() // instance
    Person.prototype.locate() //prototype
    // p.__proto__ === Person.prototype
    
  • 可以把方法定义在类构造函数或类块中,但不能在类块内给原型添加原始值或对像作为成员数据

    class Person {
      name: 'Jack'
    }
    // 报错
    
  • 类方法等同于对象属性,可以使用字符串、符号或计算的值作为键

    const symbolKey = Symbol('symbolKey')
    class Person {
      stringKey() {
        
      }
      [symbolKey]() {
        
      }
      ['computed' + 'Key']() {
        
      }
    }
    let p = new Person()
    p.stringKey()
    p[symbolKey]()
    p.computedKey()
    
  • 类定义也支持设置和获取访问器,和普通对象一样

    class Person {
      set name(newName) {
        this._name = newName
      }
      get name() {
        return this._name
      }
    }
    
3、静态类方法
  • 用于执行不特定于实例的方法

  • 不要求存在类的实例

  • static作为前缀,静态成员中this引用类本身

    class Person {
     constructor() {
        this.locate = () => console.log('instance', this)
      }
      locate() {
        console.log('prototype',this)
      }
      static locate() {
        console.log('class', this)
      }
    }
    let p = new Person()
    p.locate() 
    // instance Person {}
    Person.prototype.locate() 
    //prototype {constructor:...}
    Person.locate() 
    // class class Person{}
    
  • 静态类方法适合作为实例工厂

4、非函数原型和类成员
  • 类定义并不显式支持在原型或类上添加成员数据,但在类定义外部可以手动添加。

    class Person {
      sayName() {
        console.log(`${Person.greeting} ${this.name}`)
      }
    }
    // 在类上定义数据成员
    Person.greeting = 'My name is'
    // 在原型上定义数据成员
    Person.prototype.name = 'Jake'
    let p = new Person()
    p.sayName() // My name is Jake
    
  • 类定义中之所以没有显式支持添加数据成员,因为在共享目标(原型和类)上添加可变(可修改)数据成员是一种反模式。一般对象实例应该独自拥有通过this引用的数据

5、迭代器与生成器方法
class Person {
 // 在原型上定义生成器方法
  *createNicknameIterator() {
    yield 'Jack';
    yield 'Jake';
    yield 'J-Dog'
  }
  
  // 在类上定义生成器方法
  static *createJobIterator() {
    yield 'Butcher';
    yield 'Baker';
    yield 'Candlestick maker'
  }
}

let jobIter = Person.createJobIterator()
jobIter.next().value // Butcher
jobIter.next().value // Baker
jobIter.next().value // Candlestick maker

let p = new Person()
let nicknameIter = p.createNicknameIterator()
nicknameIter.next().value // Jack
nicknameIter.next().value // Jake
nicknameIter.next().value // J-Dog
  • 因为支持生成器方法,可以通过添加一个默认迭代器,把类实例变为可迭代对象
class Person {
  constructor() {
    this.nickname = ['Jack', 'Jake', 'J-Dog']
  }
  *[Symbol.iteator]() {
    yield *this.nickname.entries()
  }
}
let p = new Person()
for(let [idx, nickname] of p) {
  console.log(nickname)
}
  • 也可以只返回迭代器实例
class Person {
  constructor() {
    this.nickname = ['Jack', 'Jake', 'J-Dog']
  }
  [Symbol.iteator]() {
    return this.nickname.entries()
  }
}
let p = new Person()
for(let [idx, nickname] of p) {
  console.log(nickname)
}

4、继承

  • 继承虽然是新语法,但背后依旧使用的是原型链

1、继承基础

  • ES6支持单继承,使用extends关键字,就可以继承任何拥有[[Construct]]和原型的对象
  • 不仅可以继承类也可以继承普通构造函数
class Vehicle {}
// 继承类
class Bus extends Vehicle {}
let b = new Bus()
b instanceof Bus  // true
b instanceof Vehicle // true

function Person()
// 继承普通构造函数
class Engineer extends Person {}
let e = new Engineer()
e instanceof Engineer // true
e instanceof Person // true
  • 派生类都会通过原型链访问到类和原型上定义的方法。

    • this的值会反映调用相应方法的实例或者类
    class Vehicle {
      identifyPrototype(id) {
        console.log(id, this)
      }
      static identifyClass(id) {
        console.log(id, this)
      }
    }
    class Bus extends Vehicle {}
    let v = new Vehicle()
    let b = new Bus()
    
    b.identifyPrototype('bus') // bus Bus{}
    v.identifyPrototype('vehicle') // vehicle Vehicle{}
    
    Bus.identifyClass('Bus') // Bus class Bus {}
    Vehicle.identifyClass('Vehicle') // Vehicle class Vehicle {}
    
  • extends 也可以在类表达式使用,如 let Bar = class extends Foo{}

2、构造函数、HomeObject和super()

  • 派生类的方法可以通过super来引用它们的原型

  • 只能在派生类中使用,而且仅限类构造函数、实例方法和静态方法内部

  • 类构造函数中super可以调用父类构造函数

    class Vehicle {
      constructor() {
        this.hasEngine = true
      }
    }
    
    class Bus extends Vehicle {
      constructor() {
        // 不要在super之前用this
        super() //等同于super.constructor()
        console.log(this instanceof Vehicle) // true
        console.log(this) // Bus {hasEngine: true}
      }
    }
    
    new Bus()
    
  • 在静态方法中可以通过super调用继承的类上定义的静态方法

    class Vehicle {
      static identify() {
        console.log('vehicle')
      }
    }
    
    class Bus extends Vehicle {
      static identify() {
        super.identify()
      }
    }
    
    Bus.identify() // vehicle
    
  • [[HomeObject]]

    • ES6给类构造函数和静态方法添加的内部特征
    • 是一个指针,指向定义改方法的对象
    • 这个指针是自动赋值的,只能在JavaScript引擎内部访问
    • super始终会定义为[[HomeObject]]的原型
  • 使用super需要注意

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

      class Bus {
        constructor() {
          super()
          //报错
        }
      }
      
    • 不能单独引用super,要么调用构造函数,要么用它引用静态方法

    • 调用super() 会调用父类构造函数,并将返回的实例复制给this

      class Vehicle {}
      class Bus extends Vehicle {
        constructor() {
          super()
          console.log(this instanceof Vehicle)
        }
      }
      new Bus() // true
      
    • super() 的行为如同调用构造函数,如需给父类构造函数传参需要手动传入。

      class Vehicle {
        constructor(licensePlate) {
          this.licensePlate = licensePlate
        }
      }
      
      class Bus extends Vehicle {
        constructor(licensePlate) {
          super(licensePlate)
        }
      }
      
      new Bus('1222') // Bus {licensePlate: '1222'}
      
    • 如果没有定义类构造函数,在实例化派生类时会调用super() ,而且会传入所有传给派生类的参数。

      class Vehicle {
        constructor(licensePlate) {
          this.licensePlate = licensePlate
        }
      }
      
      class Bus extends Vehicle {}
      new Bus('1122') // Bus {licensePlate: '1122'}
      
    • 在类构造函数中,不能在调用super() 之前引用this

    • 如果在派生类中显式定义了构造函数,则要么必须要在其中调用super(),要么必须在其中返回一个对象

      class Vehicle {}
      class Car extends Vehicle {}
      class Bus extends Vehicle {
        constructor() {
          super()
        }
      }
      class Van extends Vehicle {
        constructor() {
          return {}
        }
      }
      new Car()	// Car {}
      new Bus()	// Bus {}
      new Van() // {}
      

3、抽象基类

  • 可以供其他类继承,但本身不会被实例化。

  • 通过new.target 可以实现,new.target保存通过new关键字调用的类或函数。通过在实例化时检测new.target是不是抽象基类,阻止对抽象基类的实例化

    class Vehicle {
      conscructor() {
      console.log(new.target)
        if(new.target === Vehicle) {
          throw new Error('....')
        }
      }
    }
    
    class Bus extends Vehicle {}
    new Bus() // class Bus {}
    new Vehicle() // class Vehicle {} 
    // Error: ....
    
  • 通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法。因为原型方法在调用类构造函数之前就存在了,所以可以通过this关键字检查相应方法

    class Vehicle {
      constructor() {
      if(new.target === Vehicle) {
          throw new Error('.....')
        }
        if(!this.foo) {
          throw new Error('Inheriting class ...')
        }
        console.log('success!')
      }
    }
    class Bus extends Vehicle {
     foo() {}
    }
    class Van extends Vehicle {}
    new Bus() // success
    new Van() // Error: Inheriting ...
    

4、继承内置类型

  • ES6类为继承内置引用类型提供了顺畅的机制,可以来扩展内置类型

    class SuperArray extends Array {
     shuffle() {
        for(let i = this.length -1;i > 0; i--) {
          const j = Math.floor(Math.random()*(i + 1));
          [this[i], this[j]] = [this[j], this[i]];
        }
      }
    }
    let a = new SuperArray(1,2,3,4,5)
    a instanceof Array // true
    a instanceof SuperArray // true
    a // [1,2,3,4,5]
    a.shuffle()
    a // [3,1,4,2,5]
    
  • 有些内置方法会返回新实例。默认返回实例的类型和原始实例类型一致

    class SuperArray extends Array {}
    let a1 = new SuperArray(1,2,3,4,5)
    let a2 = a1.filter(x=>!!(x%2))
    a1 // [1,2,3,4,5]
    a2 // [1,3,5]
    a1 instanceof SuperArray // true
    a2 instanceof SuperArray // true
    
  • 如果想覆盖这个默认行为,可以覆盖Symbol.species 访问器,这个访问器决定在创建返回的实例时返回的类

    class SuperArray extends Array {
      static get [Symbol.species]() {
        return Array
      }
    }
    let a1 = new SuperArray(1,2,3,4,5)
    let a2 = a1.filter(x=>!!(x%2))
    a1 // [1,2,3,4,5]
    a2 // [1,3,5]
    a1 instanceof SuperArray // true
    a2 instanceof SuperArray // false
    

5、类混入

  • 把不同类的行为集中到一个类,没有显式支持多类继承,但可以通过现有特征模拟

  • Object.assign()方法是为了混入对象行为而设计的。在需要混入类的行为是才有必要自己实现混入表达式,如果只是混入多个对象的属性使用Object.assign()即可。

    class Vehicle {}
    function getParentClass() {
      console.log('xxx')
      return Vehicle
    }
    class Bus extends getParentClass() {}
    // 可求值的表达式
    
  • 混入模式可以通过在一个表达式中连缀多个混入元素实现

  • 最终会被解析为一个可以被继承的类。

  • 如果Person类需要组合A、B、C,则需要某种机制实现B继承A,C继承B,Person继承C

  • 策略:定义一组“可嵌套”的函数,每个函数分别接收一个超类作为参数,而将混入类定义为这个参数的子类,并返回这个类。这些组合函数可以连缀使用

    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)))
    
    function mix(BaseClass, ...Mixins) {
      return Mixins.reduce((accumulator, current)=> current(accumulator), BaseClass)
    }
    
    class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin)
    let b = new Bus()
    b.foo()
    b.bar()
    b.baz()
    
  • 目前大多数框架都放弃混入模式,转向组合模式(把方法提取到独立的类和辅助对象里,然后把它们组合起来,但不使用继承),反映了"组合胜过继承"的原则,更灵活。