JavaScript – 理解 Function, Object, Class, Prototype, This, Mixins

前言

JavaScript (简称 JS) 有几个比较难懂的概念 -- Function、Object、Class、Prototype、This。

这主要是因为 JS 在设计之初需求比较简单 (作为脚本语言),作者没有把它当作一门正儿八经的语言来看待。

而后来在不断打补丁的过程中,又必须向后兼容,于是造就了各种奇葩现象,最终苦了我们这批学习者。

本篇将带你理解 JS 中 Function、Object、Class、Prototype、This 的基本概念。

这对往后我们深入学习前端框架 (如 Angular) 至关重要。

 

Function

JS 是一门极其灵活的语言,它即支持面向对象编程 (Object-Oriented Programming,简称 OOP),也支持函数式编程 (Functional Programming,简称 FP)。

你会在 JS 中看到许多巧妙的特性,既满足 OOP,又满足 FP。

我们就先从 Functional (函数) 说起吧 🚀。

What is Function?

何为函数?函数是一组执行过程的封装。

下面是两行执行命令,我们可以把它视为一个执行过程。

console.log('Hello World');
console.log('I am Derrick');

透过函数,我们可以把这个执行过程封装起来。

// 把执行过程封装进一个函数里面
function sayHelloWorld() {
  console.log('Hello World');
  console.log('I am Derrick');
}

sayHelloWorld(); // 调用函数执行函数内的命令

调用函数就会执行整个过程,这样就起到了代码 copy paste 的作用。

parameters & return

为了让封装更灵活,函数支持参数,透过参数来调整执行过程。

function sayHelloWorld(name) {
  console.log('Hello World');
  console.log(`I am ${name}`);
}

sayHelloWorld('Derrick');

name 就是参数。

执行过程可以产生副作用,也可以生成结果,所以函数还支持返回值。

function generateFullName(firstName, lastName) {
  return firstName + ' ' + lastName; 
}

console.log(generateFullName('Derrick', 'Yam')); // 'Derrick Yam'

函数出没的地方

函数是一等公民,我们可以直接定义它

function myFunction1(param1){
  return 'return value';
}

可以 assign 给变量

const myFunction2 = function (param1){
  return 'return value';
};

可以当参数传

[].map(function (value, index) {
  return 'value';
});

可以当函数返回值

function myFunction1() {
  return function () {};
}

宽松的参数数量

参数的数量是没有严格规定的。

举例:函数声明了一个参数,但在调用时,却传了超过一个参数,这样是 ok 的。

// 函数只需要一个参数
function myFunction1(param1) {
  console.log(param1); // 1
}

myFunction1(1, 2, 3); // 但调用时却传了三个参数

传多可以,传少也可以,甚至不传都可以。

function myFunction1(param1) {
  console.log(param1); // undefined
}
myFunction1();
myFunction1(undefined); // 和上一行是等价的

参数的默认值 (es6)

function myFunction1(param1 = 'default value') {
  console.log(param1); 
}
myFunction1(undefined); // 'default value'
myFunction1();          // 'default value'
myFunction1('value');   // 'value'

用 "等于号" 设置 default parameter value

arguments 对象

当参数数量不固定时,可以通过 arguments 对象获取最终调用时传入的参数

function myFunction1(param1 = 'value') {
  console.log(arguments[0]); // a
  console.log(arguments[1]); // b
  console.log(arguments[2]); // c
  console.log(arguments.length); // 3
}

myFunction1('a', 'b', 'c');
myFunction1(); // arguments.length = 0 (arguments 不看 default value)

类似 C# 的 params 关键字。

宽松的返回值

即便函数没有返回值,但调用者还是可以把它 assign 给变量,默认的返回值是 undefined。

这个和参数 undefined 概念是同样的。

function myFunction1() {}
const returnValue = myFunction1(); // undefined

总结

以上就是函数的基础知识。

如果你觉得很简单,那就对了。

因为我刻意避开了容易造成混乱的特性,下面会再补上复杂的。

好,我们去下一 part 🚀。

 

Object

JS 的 Object,又称 "对象",就是 OOP 里的 "对象"。

但有别于纯正的 OOP 语言 (C# / Java),JS 也支持 FP,再加上它是一门动态类型语言,这导致了我们无法直接用 C# / Java 的对象概念去理解 JS 的对象。

结论:要理解 JS 对象,我们需要从一个新的视角切入,然后才慢慢关联到其它纯正 OOP 语言的概念,这样才能把它融会贯通。

object simple look

JS 的对象长这样

const person = {
  firstName : 'Derrick',
  age: 11,
  married: false,
}

知识点:

  1. 结构

    对象的结构是 key-value pair。

    firstName 是 key,'Derrick' 是 value。

  2. 类型

    const symbolKey = Symbol('key');
    const person = {
      [symbolKey]: 'value', 
      10: null,
      '10': { },
      sayHi: function() {}
    }

    key 的类型可以是 string、number、symbol。

    注:key 10 和 key '10' 是完完全全等价的哦,所以你也可以认为它其实只支持 string 和 symbol 而已。

    value 则可以是任何类型,如:null,undefined,function,嵌套对象等等,通通都可以。

  3. no need class

    C# / Java 要创建对象必须先定义 class。

    JS 是动态类型语言,因此创建对象是不需要定义 class 的。

  4. naming

    纯正的 OOP 语言通常会把对象的 key 称之为属性 (property),如果 value 类型是函数,那会改称为方法 (method)。

    在 JS 则没有分那么清楚,抽象都叫 key,但如果你想表达多一点也可以叫属性或方法。

get, set value

const person = {
  firstName : 'Derrick',
  age: 11,
  married: false,
}

get value by key

console.log(person.firstName); // 'Derrick'

get value by string

const key = 'firstName';
console.log(person[key]); // 'Derrick'

get missing key will return undefined

console.log(person.lastName); // undefined

set value

person.firstName = 'Alex';
console.log(person.firstName); // 'Alex'

提醒:JS 是动态类型语言,set value 的类型不需要和之前的一样,可以换成任何类型。

add, delete key

JS 是动态类型语言,对象的 key 可以动态添加和删除。

const person = {
  firstName : 'Derrick',
  age: 11,
  married: false,
}

add key

person.lastName = 'Yam';
console.log(person.lastName); // 'Yam'

和 set value 的写法一样,如果 key 不存在就 add,key 已存在就 set。

delete key

delete person.firstName;
console.log(person.firstName); // undefined

对象方法

const person = {
  firstName: 'Derrick',
  lastName: 'Yam',
  sayMyName: function() {
    console.log(`I am ${person.firstName} ${person.lastName}`)
  }
};
person.sayMyName(); // 'I am Derrick Yam'

由于方法调用的时候,对象已经完成定义了,因此方法内可以直接引用 person 对象。

另外,上面是比较 old school 的语法了,新潮的写法如下

const person = {
  firstName: 'Derrick',
  lastName: 'Yam',
  // 省略了 function keyword
  sayMyName() {
    console.log(`I am ${person.firstName} ${person.lastName}`)
  }
};

它俩是完全等价的,只是写法不同而已。

getter

const person = {
  firstName : 'Derrick',
  lastName : 'Yam',
  get fullName() {
    return person.firstName + ' ' + person.lastName;
  }
}

console.log(person.fullName); // 'Derrick Yam';
person.firstName = 'Alex';
console.log(person.fullName); // 'Alex Yam';

在方法前面加一个 'get' keyword,它就变成 getter 属性了。

setter

有 getter 自然也有 setter,玩法大同小异

const person = {
  firstName : 'Derrick',
  lastName : 'Yam',
  get fullName() {
    return person.firstName + ' ' + person.lastName;
  },
  set fullName(value) {
     const [firstName, lastName] = value.split(' ');
     person.firstName = firstName;
     person.lastName = lastName;
  }
}

person.fullName = 'Alex Lee';
console.log(person.firstName); // 'Alex'
console.log(person.lastName); // 'Lee'

在方法前加一个 'set' keyword,它就变成 setter 了。

它的参数就是要 assign 的 value。

get all keys & values

const person = {
  firstName : 'Derrick', // string property
  lastName : 'Yam',      // string property
  10: '',                // number property
  [Symbol()]: '',        // symbol property

  // getter
  get fullName() {
    return person.firstName + ' ' + person.lastName;
  },

  // setter 
  set fullName(value) {
    const [firstName, lastName] = value.split(' ');
    person.firstName = firstName;
    person.lastName = lastName;
  },

  // method
  sayName() {
    return person.fullName;
  },
}

想获取所有的 keys 和 values 可以透过以下几个方法:

  1. Object.keys

    获取所有 keys (属性和方法),但不包括 symbol

    const allKeys = Object.keys(person); 
    console.log(allKeys); // ['10', 'firstName', 'lastName', 'fullName', 'sayName']

    三个知识点:

    a. 不包含 symbol property

    b. number property 变成 string 了

    c. getter setter fullName 只代表一个 key。

  2. Object.values

    获取所有 values

    const allValues = Object.values(person); 
    console.log(allValues); // ['', 'Derrick', 'Yam', 'Derrick Yam', function]

    Object.values 只是一个方便,它完全等价于 

    const allValues = Object.keys(person).map(key => person[key]);

    因此,symbol property 依然不包含在内。

  3. Object.entries

    获取所有 keys 和 values (它也只是一个方便,依然是基于 Object.keys)

    const allKeyAndValues = Object.entries(person); 
    console.log(allKeyAndValues); 
    /* 
      返回的类型是 array array
      [
        ['10', ''],
        ['firstName', 'Derrick'],
        ['lastName', 'Yam'],
        ['fullName', 'Derrick Yam'],
        ['sayName', function],
      ]
    */
    // 搭配 for of 使用
    for (const [key, value] of allKeyAndValues) {
      console.log(key, value); // '10', ''
    }
  4. Object.getOwnPropertySymbols

    const allSymbols = Object.getOwnPropertySymbols(person);
    console.log(allSymbols); // [Symbol()]

    Object.keys, values, entries 都遍历不出 symbol property;只有 Object.getOwnPropertySymbols 可以。

    反过来 Object.getOwnPropertySymbols只能遍历出 symbol property;string 和 number 都遍历不出来。

Property Descriptor

对象 key 有一个 Descriptor 概念,我们可以把它视作一个 configuration。

看例子:

  1. writable

    const person = {
      firstName : 'Derrick',
      lastName : 'Yam',
    }
    
    // 设置 person.firstName 的 Descriptor
    Object.defineProperty(person, 'firstName', {
      writable: false // disable set 
    });
    
    person.firstName = 'New Name'; // try set new value
    console.log(person.firstName); // 'Derrick', still the same value, assign value not working anymore

    透过 Object.defineProperty 来设置 key 的 Descriptor,它有好几个东西可以配置,writable 是其中一个。

    writable: false 意思就是让这个 key 不可以被写入 -- disable set 的功能。

    disabled 之后这个 value 就不能改变了,所有 assign value 的操作将被无视。

    另外,writable 也适用于 symbol key。

  2. enumerable

    Object.defineProperty(person, 'firstName', {
      enumerable: false
    });
    
    const allKeys = Object.keys(person); // ['lastName'], firstName 无法被遍历出来
    enumerable: false 表示这个 key 无法被遍历出来。

    Object.keys, values, entries 会无视 enumerable: false 的 key。

    如果我们想遍历出所有的 keys,包括 enumerable: false 的话,需要使用 Object.getOwnPropertyNames

    Object.defineProperty(person, 'firstName', {
      enumerable: false
    });
    
    const enumerableKeys = Object.keys(person); // ['lastName'], firstName 无法被遍历出来
    const allKeys = Object.getOwnPropertyNames(person); // ['firstName', 'lastName'] 两个 keys 都能遍历出来

    另外一点,enumerable 对 symbol key 是无效的,虽然 symbol key 的 enumerable by default 是 false,但即便我们将它设置成 true,它依然无法被 Object.keys 遍历出来,

    Object.getOwnPropertySymbols 则不管 enumerable 是 true 还是 false,它都会遍历 symbol property 出来。

  3. configurable

    Object.defineProperty(person, 'firstName', {
      writable: false,    // 改了 writable
      configurable: false // 不允许后续再修改了
    });
    
    // 尝试修改会直接报错 TypeError: Cannot redefine property: firstName
    Object.defineProperty(person, 'firstName', {
      writable: true,
      configurable: true
    });

    configurable: false 表示这个 Descriptor 无法再被修改,再尝试修改会直接报错。

  4. getter, setter, value

    Object.defineProperty 不仅仅可以用来设置 Descriptor,或者说 Descriptor 不仅仅是 configuration。

    我们看例子:

    Object.defineProperty(person, 'firstName', {
      value: 'New Name'
    });
    
    console.log(person.firstName); // New Name

    define value 等价于 assign value

    person.firstName = 'New Name';

    效果一模一样。

    同样的,假如 firstName property 不存在,那会变成 add new property。

    define getter setter 也没问题

    Object.defineProperty(person, 'fullName', {
      get() {
        return person.firstName + ' ' + person.lastName;
      },
      set(value) {
         const [firstName, lastName] = value.split(' ');
         person.firstName = firstName;
         person.lastName = lastName;
      }
    });
    
    console.log(person.fullName);  // Derrick Yam
    
    person.fullName = 'Alex Lee';
    
    console.log(person.firstName); // Alex
    console.log(person.lastName);  // Lee

    唯一要注意的是,getter setter 和 value writable 是有冲突的,比如说:我们同时 define getter 和 value 的话,它会报错

    因为这样不逻辑,都 getter 了怎么还会有 value 呢?

获取 Property Descriptor

除了可以 define,我们也可以用 Object.getOwnPropertyDescriptor 方法查看当前的 Descriptor。

const person = {
  firstName : 'Derrick',
  lastName : 'Yam',
}
 
const descriptor = Object.getOwnPropertyDescriptor(person, 'firstName');
console.log(descriptor); // { configurable: true, enumerable: true, writable: true, value: 'Derrick' }

使用 person.firstName = '' 创建的 key, configurable, enumerable, writable 默认都是 true。

但使用 Object.defineProperty 创建的 key, configurable, enumerable, writable 默认都是 false。

const person = {
  firstName : 'Derrick',
}

Object.defineProperty(person, 'lastName', { value: 'Yam' });
const lastNameDescriptor = Object.getOwnPropertyDescriptor(person, 'lastName');
console.log(lastNameDescriptor); // { configurable: false, enumerable: false, writable: false, value: 'Yam' }

注:只有在添加新 key 时,默认才会是 false。如果 key 本来已存在,Object.defineProperty 若没有额外声明 configurable, enumerable, writable,则会保持原有的;若有声明,则会覆盖。

批量设置 Property Descriptor

const person = {
  firstName : 'Derrick',
  lastName: 'Yam'
}

Object.defineProperties(person, {
  firstName: {
    writable: false
  },
  lastName: {
    writable: false
  },
  fullName: {
    get() {
      return person.firstName + ' ' + person.lastName;
    },
    enumerable: true,
    configurable: true,
  }
});

没什么特别的,只是一个上层封装,让它可以批量设置而已。

总结

以上就是 Object 的基础知识。

如果你觉得很简单,那就对了。

因为我刻意避开了容易造成混乱的特性,下面会再补上复杂的。

好,我们去下一 part 🚀。

 

Class

有对象,有函数,其实已经可以做到 OOP 了。

但是对比其它纯正 OOP 语言 (C#, Java),JS 还少了一个重要的特性 -- Class。

OOP 语言怎么可以少了 class 呢?

于是 es6 推出了 class 语法糖,oh yeah 😎。

等等...为什么是语法糖而不是语法?

因为早在 es6 之前,JS 已经能实现 class 的特性了。

只不过它是利用 Object + Function + Prototype 这三种特性间接实现的。

这种间接的方式有许多问题:难封装,难理解,代码又碎。

后来,随着越来越多人使用 JS,越来越多人骂,为了平息众怒,ECMA 就另外推出了语法糖 -- class。

上面在讲解 Object 和 Function 时,我刻意避开了与 class 相关的知识,所以它们才那么直观好理解;一旦加入 class 相关知识,理解难度就上去了。

我们先搞清楚 JS 上层的 class 语法糖,之后再去看底层 Object + Function + Prototype 是如何实现 class 特性的,Let's go 🚀

注:说 class 只是语法糖,其实不是很严谨,虽然几乎 99% 的特性确实可以靠非 class 来模拟,但仍然有一些例外。不过哪些例外几乎不会出现在日常编程中,所以我们把 class 当成语法糖来看待会更好理解。

Class Overview

首先,我们要知道 class 对 JS 而言并不是增加了语言的能力,它只是增加了语言的管理而已。

就好比说,JS 要是少了 for, while 和递归,那么语言就少了 looping 的能力,但如果只是少了 for 和 while,那任然可以用递归的方式实现 looping,只是代码不好管理而已。

class 长这样

class Person {
  constructor(firstName, age) {
    this.firstName = firstName;
    this.age = age;
  }
}

const person = new Person('Derrick', 20); // { firstName: 'Derrick', age: 20 }

class 是对象的模板,或者说是工厂,它主要的职责是封装对象的结构和创建过程。

上面有许多知识点:

  1. new

    new 是创建新对象的意思,new Person 会创建一个新的 person 对象。

    我们通常把这个对象叫做 instance (实例)。

  2. constructor and new Person

    constructor 就像一个函数,new Person() 就像一个函数调用。

    它们之间可以传递参数,这使得创建过程变得更灵活。

  3. this

    constructor 中的 this 指向即将被创建的新对象 (实例)。

    this.firstName = firstName; 就是给这个实例添加一个 firstName 属性,然后把参数 firstName assign 给它作为 value。

用 Object 和 Function 来表达的话,大概长这样:

function newPerson(firstName, age) {
  return {
    firstName: firstName,
    age: age
  }
}
const person = newPerson('Derrick', 20);

当然,这个只是表达,实际上要用 Function + Object + Prototype 做出正真的 class 特性是非常复杂的,这个我们下一 part 才讲,先继续看 class 的其它特性。

术语

一些和 class 相关的术语:

  1. Object 是对象

  2. Class 是类

  3. instance 是实例,通过 new Class() 创建出来的对象就叫实例,这个创建过程叫实例化。-- Instance ≼ Object (实例是一种对象)

  4. key value 是键值,它指的是对象的内容 { key: 'value' }

  5. property 是属性,method 是方法,当一个对象 key 的 value 类型是函数时,这个 key 被称为方法,当类型是非函数时,这个 key 被称为属性。-- property/method ≼ Key (属性和方法是一种键)

  6. constructor 是构造函数,构造什么呢?构造出实例的意思 -- constructor ≼ function (constructor 是一种函数) 

Class 的特性

1. constructor (构造函数)

class Person {
  constructor(firstName, age) {
    this.firstName = firstName;
    this.age = age;
  }
}

constructor 的职责是构造出实例,构造指的是给实例设置属性和方法。每一次 new Person(),constructor 都会被执行,然后返回一个新实例 (虽然 constructor 里并没有写 return)。

constructor 里的 this 指向当前创建的新实例,this.firstName = firstName 就是往这个新实例添加属性。

另外,constructor 里其实是可以写 return 的,return 的类型必须是对象,然后这个对象将作为 new Person 创建的实例。

虽然可以这么做,但这样做很奇怪,没有逻辑,行为也和其它语言也不一致,所以不要这么干,乖乖用 this 让它自然 return this 就好了。

2. define property

在 class 里,想给实例添加属性有两种方式,这两种方式在一些情况下会有区别,但只是一些情况而已。

  1. inside constructor

    class Person {
      constructor(firstName, age) {
        this.firstName = firstName;
        this.age = age;
      }
    }

    在 constructor 里 define 属性的方式就是往 this 添加属性。

    这里 this 指向实例,而实例是一种对象。所以,我们怎么给对象添加属性就怎么给 this 添加属性 (比如:使用 Object.defineProperty 也可以)。

  2. outside constructor

    class Person {
      firstName = 'Derrick';
      lastName = 'Yam'
    }
    
    // 等价于
    
    class Person {
      constructor (){
        this.firstName = 'Derrick';
        this.lastName = 'Yam'
      }
    }

    对 define 属性来说,inside 和 ouside constructor 没有什么区别,唯一的区别就是 outside 无法使用到 constructor 参数而已。

3. define method

在讲解 define 方法之前,我们需要朴上一个知识点 -- 对象方法里的 this。

上面我们有提到,constructor 里的 this 是指向新实例。

这种函数 (constructor) 中使用 this 的概念,同样也适用于对象方法里。

const person = {
  firstName : 'Derrick',
  lastName : 'Yam',

  get fullName() {
    return this.firstName + ' ' + this.lastName;
  },

  set fullName(value) {
    const [firstName, lastName] = value.split(' ');
    this.firstName = firstName;
    this.lastName = lastName;
  },

  sayMyName() {
    console.log(this.fullName);
  },
}
person.sayMyName(); // 'Derrick Yam'
person.fullName = 'Alex Lee';
console.log(person.firstName); // 'Alex'
console.log(person.lastName); // 'Lee'

get fullName, set fullName, sayMyName 这三个方法里的 this 都指向 person 对象。

好,回到主题 -- define 方法。

define 方法和 define 属性一样,使用 inside 和 outside constructor 两种方式。

但是呢,这两种方式 define 出来的方法是有区别的哦。

  1. inside constructor

    class Person {
      constructor(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    
        // define getter 
        Object.defineProperty(this, 'fullName', {
          get() {
            return this.firstName + ' ' + this.lastName;
          }
        });
    
        // define method
        this.sayMyName = function() { 
          console.log(this.fullName);
        }
      }
    }
    
    const person = new Person('Derrick', 'Yam');
    person.sayMyName(); // 'Derrick Yam'

    在 constructor 里 define 方法的方式就是往 this 添加方法。

    这里 this 指向实例,而实例是一种对象。所以,我们怎么给对象添加方法就怎么给 this 添加方法。

    inside define 就代码而言挺丑的,对比我们创建对象的写法

    const person = {
      get fullName(){},
      sayMyName() {}
    }
    
    class Person {
      constructor() {
        Object.defineProperty(this, 'fullName', {
          get() {}
        });
        this.sayMyName = function() { }
      }
    }

    之所以会有那么大的区别,是因为 constructor 里不是创建对象,而是给对象添加方法,所以写法会差这么多了。

  2. outside constructor

    class Person {
      constructor(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
      }
    
      get fullName() {
        return this.firstName + ' ' + this.lastName;
      }
    
      sayMyName() {
        console.log(this.fullName);
      }
    }

    outside constructor 的写法就和创建对象非常相识了,不像 inside define 那么繁琐。

    除了代码好看以外,outside 和 inside 还有两大的区别:

    a. outside define 的方法不会被 Object.keys 遍历出来

    const person = new Person('Derrick', 'Yam');
    console.log(Object.keys(person)); // ['firstName', 'lastName']

    fullNamesayMyName 都不在 key list 里。

    它们不在 key list 不是因为 enumerable: false,是其它原因造成的,这个细节下面会再讲解。

    至于,inside define 的话 sayMyName 会在 key list 里,fullName 则不会,因为 fullName 是透过 Object.defineProperty 添加的,默认是 enumerable: false

    b. outside define 的方法是共享的

    const person1 = new Person('Derrick', 'Yam');
    const person2 = new Person('Derrick', 'Yam');
    console.log(person1.sayMyName === person2.sayMyName); // true

    不同实例,但方法 reference 是同一个。

    如果是 inside define,那每一个实例的方法都是不同的 reference。

    其实 outside define 的效果会更好,毕竟方法内容是一样的,共享 reference 可以节省内存。

    如果 inside define 要做到相同效果,那代码会变得更丑😅

    function sayMyName() {
      console.log(this.fullName);
    }
    
    function fullName () {
      return this.firstName + ' ' + this.lastName;
    }
    
    class Person {
      constructor(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.sayMyName = sayMyName;
        Object.defineProperty(this, 'fullName', {
          get: fullName
        })
      }
    }

    outside define 如何让所有实例共享一个方法 reference,我们下面会再讲解。

  3. outside define but use old school syntax

    下面这两个写法效果是不一样的哦

    class Person {
      // old school syntax
      sayMyName = function () {
        return '';
      }
    
      // regular syntax
      sayMyName() {
        return ''
      }
    }

    outside define method by old school syntax 等价于 inside define method

    下面这两句是完完全全等价的

    class Person {
      constructor() {
        this.sayMyName = function() {
          return ''
        }
      }
      sayMyName = function () {
        return '';
      }
    }

    我们不需要理解为什么它会这样,我们不要这样写就可以了。

    我们要嘛使用 inside define,要嘛使用 outside define,但不要用 old school syntax。

4. define private key (a.k.a private field)

private key (属性或方法) 是一个比较新的概念,es2022 才推出的。

所谓 private 的意思是,只有在对象里的方法可以访问到 private key。

const person = {
  privateProperty: 'value',
  method (){
    console.log(this.method); // 这里可以访问
  }
}
console.log(person.privateProperty); // 这里不可以访问,要返回 undefined

enumerable: false 只是遍历不出来,但是直接 get value by key 仍然是可以访问到的。

所以 JS 需要一种新的机制才能做到正真的 private。

define private key

class Person {
  #privateValue
  constructor() {
    this.#privateValue = 'value'
  }

  method() {
    console.log(this.#privateValue);
    console.log(this['#privateValue']); // 会得到 undefined,因为 private key 只能用 dot notation 访问,不能用 bracket notation 访问
  }
}

const person = new Person();
console.log(Object.keys(person));  // [] empty array
person.method(); // 'value'
console.log(person.#privateValue); // IDE Error: Private field '#privateValue' must be declared in an enclosing class

几个知识点:

  1. 一定要使用 class

  2. 一定要使用 outside constructor define (不一定要放 default value,但一定要 define)

  3. 只有在 class 方法里才可以访问 private key,通过实例访问 private key 会直接报错

  4. 没有任何一种方式可以遍历出 private key。

  5. private key 只能用 dot notation 访问(this.#privateValue 可以),不能用 bracket notation 访问(this['#privateValue'] 不行)。

5. static key (a.k.a static field) & block

class Person {
  static publicName = 'public';
  static #privateName = 'private';
  static {
    console.log('before class Person done');
    console.log(Person.#privateName); // value
  }
}
console.log('class Person done');

console.log(Person.publicName); // public
console.log(Person.#name); // error: Property '#name' is not accessible outside class 'Person' because it has a private identifier

静态 key (属性或方法) 不属于实例,它们属于 class 本身。使用方式是 Class.staticKey (e.g. Person.publicName)

另外,静态 key 也可以是 private 的。

static block 是一个 IIFE 立即调用函数,注意看上面的 log('before class Person done') 执行会早于 log('class Person done')

6. instanceof 和 constructor

const person = new Person();
console.log(person instanceof Person); // true;
console.log(person.constructor === Person); // true;

透过 instanceof 我们可以判断某个对象是否属于某个 class 的实例。

也可以通过 instance.constructor 获取它的 class。

注:instanceof 并不是透过 instance.constructor 来做判断的哦,instanceof 还会顾虑继承的情况,细节下面会再讲解。

7. inherit 继承 (extends)

class Parent {
  constructor(name) {
    this.name = name;
  }

  parentMethod() {}

  sameNameMethod() {}
}

class Child extends Parent {
  constructor(name, age) {
    super(name); // 一定要在 add property 之前调用 super
    this.age = age;
  }

  childMethod() {}

  // override Parent method
  sameNameMethod() { 
    // do something decorate
    super.sameNameMethod(); // call parent method
    // do something decorate
  }
}

const child = new Child('Derrick', 11);
child.parentMethod();    // call parent method
child.childMethod();     // call child method
console.log(child.name); // get parent property
console.log(child.age);  // get parent property

console.log(child instanceof Child);      // true
console.log(child instanceof Parent);     // true
console.log(child.constructor === Child); // true

继承有几个特性:

  1. child 实例拥有所有 Parent 的属性和方法

  2. new Child 时,Parentconstructor 会被调用

  3. childChild 的实例,同时也算是 Parent 的实例,因此 child instanceof Parent 也成立。

  4. Child 可以 override Parent 的属性和方法,也可以通过 super 调用 Parent 的方法。

    注:一旦 override 了,即便是 Parent 也会调用到 override 的,这个行为和 C# 的 virtual + override 一样。

    class Parent {
      doSomething (){
        this.sameNameMethod(); 
      }
    
      sameNameMethod() {
        console.log('parent');
      }
    }
    
    class Child extends Parent {
      sameNameMethod() {
        console.log('child');
      }
    }
    
    const child = new Child();
    child.doSomething();  // 'child'

    C# 还有一个 new keyword,可以做到只有 Child 调用到 override 的,而 Parent 依然调用 original 的,但这个 JS 没有。

Class Init 执行顺序

function getString() {
  console.log('3');
  return '';
}
function getNumber() {
  console.log('1');
  return 0;
}

class Parent {
  constructor() {
    console.log('2'); // 2
  }
  age = getNumber(); // 1
}
class Child extends Parent {
  constructor() {
    super();
    console.log('4'); // 4
  }
  name = getString(); // 3
}

const person = new Child();

Class 冷知识

class Person {
  name;
  age = 11;
}
const person = new Person();
console.log(Object.keys(person)); // ['name', 'age']

name 虽然没有 init value 但依然是会有 property 的哦, value = undefined。

总结

以上就是 Class 的基础知识。

如果你觉得很简单,那就对了。

因为 class 是 es6 出的语法糖,糖自然是甜甜的,真正复杂的是它的底层实现原理,下面会再讲解。

好,我们去下一 part 🚀。

 

Prototype

Prototype (原型链) 是 JS 的一个特性,其它语言很少见。

它有点像纯正 OOP 语言 (C#, Java) 的继承概念。

不过,只是像而已,请不要把 Prototype 和 Inheritance 划上等号,它俩是不同的机制。

好,我们来看看 Prototype 机制是如何工作的,又和继承有哪些相识之处 🚀。

What's happened?

const person = { };
console.log(person.toString()); // [object Object]
console.log(person.toNumber()); // Uncaught TypeError: person.toNumber is not a function

为什么调用 toNumber 报错了,但调用 toString 却没有报错?

person 对象就是一个空对象,哪来的 toString 方法? 

再看一个更神奇的例子

const person = {};
person.__proto__ = { name: 'Derrick' };
console.log(person.name); // Derrick

__proto__ 是啥?

为什么 person.name 可以拿到 person.__proto__.name 的 value? 

原型链

每个对象都有一个隐藏属性叫 __proto__。(其实在 ECMA 规范里面我们是不可以直接访问这个属性的,只是浏览器没有遵从这个规则,所以例子里才可以这样写)

这个 __proto__ 指的就是 Prototype,它也是一个对象。

而 "每个对象都有一个 __proto__" ,所以这个 __proto__ 对象也有它自己的 __proto__ 对象🤪。

这样一个接一个 __proto__ 就构成了原型"链"。

原型链的终点

当一个对象的 __proto__ = null 时,它就是原型链的终点。

原型链查找

当我们访问 object.key 的时候,JS 引擎首先会查找 object 所有的 keys,如果没有找到这个 key,那它会去 object.__proto__ 对象中继续找。

如果找到了就会返回它的值,如果找不到就沿着原型链继续找,直到结束。

Object.prototype

const person = {};
console.log(person.__proto__ === Object.prototype); // true
console.log(Object.getPrototypeOf(person) === Object.prototype); // 正规获取 Prototype 的方式是调用 Object.getPrototypeOf,访问私有属性 __proto__ 是不正规的做法。
console.log(person.toString()); // [object Object]
console.log(person.toNumber()); // Uncaught TypeError: person.toNumber is not a function

当我们创建对象时,对象默认的 __proto__ (prototype) 是 Object.prototype 对象,而 Object.prototype__proto__ 则是 null。

整条原型链是:personObject.prototypenull 一共两个可查找的对象。

Object.prototype 对象中包含了许多方法,其中一个就是 toString方法。

当调用 person.toString 时,JS 引擎先找 person 的 keys,看有没有 toString 方法,结果没有,于是它会继续找 person.__proto__ (也就是 Object.prototype 对象),然后就找到了 toString 方法。

当调用 person.toNumber 时,也是同样的查找路线,只不过,找到最后 Object.prototype 里并没有 toString 方法,所以就报错了。

set Prototype

const person = {};
person.__proto__ = { name: 'Derrick' };
Object.setPrototypeOf(person, { name: 'Derrick' }); // 正规写法

对象的 prototype 是可以任意替换的,调用 Object.setPrototypeOf 传入对象或 null 就可以了。

创建对象同时 set Prototype

对象默认的 prototype 是 Object.prototype 对象。

我们可以透过 Object.create 方法,在创建对象时自定义对象的 prototype。

const person = Object.create({ name: 'Derrick' });
// 等价于下面
const person = {};
Object.setPrototypeOf(person, { name: 'Derrick' }); 


// 创建一个没有 __proto__ 的对象
const animal = Object.create(null);
// 等价于下面
const animal = {}; // 默认 __proto__ 是 Object.prototype 
Object.setPrototypeOf(animal, null); 

用 Prototype 特性实现 Inheritance 

Inheritance 的特色是 child 可以访问 parent 的属性和方法,我们可以利用原型链查找机制实现这个效果。

const parent = {
  name: 'Derrick',
  method() {
    console.log('done');
  },
};
const child = Object.create(parent);
console.log(child.name); // 'Derrick'
child.method(); // 'done'

小心原型链的坑

看注释理解

const parent = {
  age: 11,
  fullName: {
    firstName: '',
    lastName: '',
  },
};
const child = Object.create(parent);
console.log(child.age); // read from parent
child.age = 15;         // 注意: 这里不是 set property to parent 而是 add property to child
console.log(child.age); // 注意: 这个是 read from child

console.log(child.fullName.firstName); // read from parent
child.fullName.firstName = 'Derrick';  // 注意: 这里是 set property to parent, 而不是 add property to child,和上面不同哦
console.log(child.fullName.firstName); // 注意: 这里依然是 read from parent

以前 AngularJS 的 $scope 就使用了 Prototype 概念,这导致了许多人在 set value 的时候经常出现 bug。

原因就是上面这样,没搞清楚什么时候是 set to child,什么时候是 set to parent。

总之,get from prototype 不代表就是 set to prototype,因为对象属性是可以动态添加的,你以为是 set property,结果它是 add property。

Prototype 对 get all keys 的影响

const parent = {
  firstName: 'Derrick',
  lastName: 'Yam',
};
const child = Object.create(parent);
child.age = 20;

const allKeys = Object.keys(child);
console.log(allKeys); // ['age']

Object.keys, values, entriesgetOwnPropertyNames 都无法遍历出 prototype 的 key。

上面例子中,只有 child 自己本身 (Own) 的 key 才能被遍历出来。

如果我们想遍历所有原型链的 key,需要使用 for in 语法

const parent = {
  firstName: 'Derrick',
  lastName: 'Yam',
  sayMyName() {}
};
const child = Object.create(parent);
child.age = 20;
child.doSomething = function(){ }

for (const key in child) {
  const isOwnKey = child.hasOwnProperty(key);
  const isPrototypeKey = !child.hasOwnProperty(key);
  console.log(key, `isOwnKey: ${isOwnKey}`);
  console.log(key, `isPrototypeKey: ${isPrototypeKey}`);
  // age         isOwnKey: true
  // doSomething isOwnKey: true
  // firstName   isPrototypeKey: true
  // lastName    isPrototypeKey: true
  // sayMyName   isPrototypeKey: true
}

两个知识点:

  1. hasOwnProperty 方法可以查看 key 是 Own 还是 from Prototype。

  2. for inObject.keys 一样只能遍历出 enumerable: true 的 key,同时 symbol key 也一样是遍历不出来的。

所以说 for in 也不是万能的,如果我们真想完完全全的找出每一个 key,需要搭配使用:

  1. Object.getPrototypeOf

    获取对象的 prototype,这样就可以沿着原型链找下去。

  2. Object.getOwnPropertyNames

    获取对象所有 keys 包括 enumerable: false

  3. Object.getOwnPropertySymbols

    获取对象所有 symbol key。

Prototype 对方法 this 的影响

const parent = {
  firstName: 'Derrick',
  lastName: 'Yam',
  sayMyName() {
    console.log(this.firstName);
  }
};
const child = Object.create(parent);
child.firstName = 'Child Name';

child.sayMyName();  // 'Child Name', 此时 sayMyName 的 this 指向 child
parent.sayMyName(); // 'Derrick',    此时 sayMyName 的 this 指向 parent

注意看 child.sayMyName()child 本身没有 sayMyName 方法。

因此它会找 child.__proto__ (也就是 parent) 的 sayMyName 方法来调用。

不过,sayMyName 里头的 this 却不是指向 parent,而是指向 child

this 有点像是被偷龙转风了。

总结

Prototype 为对象添加了继承的能力,第一次看到或许会感觉有点绕,但了解机制后会发现它其实挺简单的。

就两个概念:

  1. 对象和对象关联起来后叫原型链

  2. 对象访问时会沿着原型链挨个挨个查找

好,以上就是 Prototype 的基础知识。

如果你觉得很简单,那就对了。

因为 Prototype 并没有破坏 Object 的基础知识,它只是扩展了一些特性而已。

真正复杂的是用 Prototype 去实现 class 底层的继承特性。

我们继续下一 part 吧 🚀。

 

class 和 prototype 的关系

class 和 prototype 有着紧密的关系,我们来了解一下 🚀。

instance.__proto__ & class.prototype

class Person {}
const person = new Person();
console.log(person.constructor === Person); // true

person 实例有一个 constructor 属性,它的值指向 Person class。

console.log(Object.getOwnPropertyNames(person)); // []

 不,person 实例并没有 constructor 属性。

本身没有,那就一定是在原型链中。

console.log(Object.getOwnPropertyNames(person.__proto__)); // ['constructor']
console.log(person.__proto__.constructor === Person); // true

果不其然,那 person__proto__ 指向的是谁呢?

答案是 Person.prototype

console.log(person.__proto__ === Person.prototype); // true

两个知识点:

  1. 每个 class 都会有 prototype (一个 static key) 对象,对象里有一个 constructor 属性指向回 class 本身。

    class Person {}
    console.log(Person.prototype.constructor === Person); // true
  2. 实例 (instance) 的 __proto__指向它的 class.prototype

shared method 的原理

class Dog {
  jump = function() {}
  
  bite() {}
}

const dog1 = new Dog();
const dog2 = new Dog();

console.log(dog1.jump === dog2.jump); // false
console.log(dog1.bite === dog2.bite); // true

bite 是 shared method,jump 则不是。

console.log(Object.getOwnPropertyNames(dog1)); // ['jump']

jumpdog 的 key,bite 则不是。 

什么原理?

console.log(dog1.bite === Dog.prototype.bite); // true 

bite被定义在 prototype (dog1.__proto__.bite 或者说 Dog.prototype.bite) 中,而不是在实例本身。

结论:shared method 是靠 prototype 实现的。

extends 和 prototype 的关系

class Animal {}
class Dog extends Animal {} 

const lucky = new Dog();
console.log(lucky.__proto__ === Dog.prototype); // true 
console.log(lucky.__proto__.__proto__ === Animal.prototype); // true 

luckyDog 的实例,Dog 继承自 Animal

class 的继承是靠 prototype 实现的。

Dog 继承 Animal 会导致 Dog.prototype.__proto__ === Animal.prototype

原型链:luckyDog.prototypeAnimal.prototype

原型链只负责 shared method,普通 method 和 property 是在实例本身,即便是父类也会被定义到实例本身

class Animal {
  jump = function() {} // non shared method
  run() {}             // shared method
}
class Dog extends Animal {
  bite = function () { } // non shared method 
  walk() {}              // shared method
} 

const dog1 = new Dog();

console.log(Object.keys(dog1));                 // ['jump', 'bite'],Animal(父类)的 jump 也在实例里,因为它不是 shared method
console.log(dog1.walk === Dog.prototype.walk);  // true, 原型链路线:dog1.__proto__.walk 
console.log(dog1.run === Animal.prototype.run); // true,原型链路线:dog1.__proto__.__proto__.run

instanceof 的原理

class Animal {}
class Dog extends Animal {} 

const lucky = new Dog();
console.log(lucky instanceof Dog);      // true
console.log(lucky.constructor === Dog); // true
console.log(lucky instanceof Animal);   // true

instanceof 不是依靠 lucky.constructor做判断,而是靠 __proto__ vs class.prototype

console.log(lucky.__proto__ === Dog.prototype); // true,因此 lucky instanceof Dog
console.log(lucky.__proto__.__proto__ === Animal.prototype); // true, 因此 lucky instanceof Animal

只要原型链中,其中一个对象和 class.prototype 相等,那就是 instanceof

特殊情况

console.log(new Number(0) instanceof Number); // true

这个合理。

console.log(Object.getPrototypeOf(0) === Number.prototype); // true

0 不是对象,但是有 prototype?

可能是因为 auto-boxing 的缘故,算合理吧。

console.log(0 instanceof Number); // false

0 的 prototype 是 Number.prototype 啊,怎么 instanceoffalse? 

因为这里又不 auto-boxing 了...😔

 

Function this, apply, call, arrow function, bind

上面我们有提到 this 这个关键字被用于 class 的 constructor 还有 method 中。

这个很好理解,因为纯正 OOP 语言 (C# / Java) 也是这样用的。

this in Function

不过,在 JS,this 还可以被用于 Function 中。

function doSomething () {
  console.log(this === window); // true
}

doSomething();

在浏览器环境,函数中的 this 指向 window 对象。

不过这是一个历史原因,在 'use strict' (es5) 模式下,函数中的 this 会是 undefined

'use strict'; // 使用 'use strict' 模式

function doSomething () {
  console.log(this === undefined); // true
}

doSomething();

但是!有例外:

'use strict';

function handleClick (event) {
  console.log(event.currentTarget === this); // true
}

document.body.addEventListener('click', handleClick);

在 DOM API addEventListener 的 callback 函数中,this 指向的是 event.currentTarget,而不是 undefined。

call & apply

它是怎么做到的呢?

答案是透过 call 方法调用函数,并传入 this 值。

'use strict';

function doSomething (param1, param2) {
  console.log(this);   // 'hello world'
  console.log(param1); // 'param1'
  console.log(param2); // 'param2'
  console.log(arguments.length); // 2
}

// 透过 call 方法执行 doSomething 函数
// 参数一将成为 this 值 
doSomething.call('hello world', 'param1', 'param2'); 

call 方法的第一个参数是 this 值,其它参数对应的是函数的参数。

另外,apply 方法也是相同的功能。

doSomething.call('hello world', 'param1', 'param2'); 
// 完全等价于
doSomething.apply('hello world', ['param1', 'param2']); 

唯一的区别只是第二参数是采用 array。

callapply 技巧常用来偷龙转风对象方法里的 this

const person = { 
  firstName: 'Derrick', 
  lastName: 'Yam',
  getFullName() {
    return this.firstName + ' ' + this.lastName;
  }
};

console.log(person.getFullName()); // 'Derrick Yam'

// 借用 person.getFullName 方法,但偷龙转风 this 对象
console.log(
  person.getFullName.call({ firstName: 'Alex', lastName: 'Lee' }) // 'Alex Lee'
);

类似于我们借用了某个对象的方法。 

Arrow Function

Arrow Function 是 es6 推出的,它的写法有点像 C# 的 lambda 表达式。

它和普通的 Function 有两个区别:

第一个是 this

'use strict';

const that = this;

const handleClick = (event) => {
  console.log(event.currentTarget === this); // false

  console.log(this === that); // true
}

document.body.addEventListener('click', handleClick);

arrow function 里,没有自己的 this

handleClick 函数内的 this,其实是函数外的 this 来的。

因此,call / apply 传入 this 也无用。

'use strict';

const that = this;

const handleClick = (param1) => {
  console.log(this === that); // true 依然是 that
  console.log(param1); // 'param1'
}

handleClick.call('hello world', 'param1');

第二个区别是:arrow function 里没有 arguments 对象

const handleClick = () => {
  console.log(arguments); // Error: arguments is not defined
}

尝试访问 arguments 对象会直接报错。

少了 arguments,我们可以改用 rest parameters (es6) 来获取动态的参数。

const handleClick = (...args) => {
  console.log(args.length); // 2
}

handleClick('1', '2');

另外,对象方法采用 arrow function 的话,一样会没有 this 哦:

'use strict';

const that = this;

const person = {
  // function old school 写法
  method1: function () { 
    console.log(this === person); // true
  },

  // function modern 写法
  method2() {
    console.log(this === person); // true
  },

  // arrow function
  method3: () => {
    console.log(this === that); // true
  }
}

person.method1();
person.method2();
person.method3();

bind

bind 方法的作用是封装 parameters。

看例子

'use strict';

function doSomething (param1, param2) {
  console.log(this); // 'this'
  console.log(param1); // 'param1'
  console.log(param2); // 'param2'
  console.log(arguments.length); // 2
}

const boundDoSomething = doSomething.bind('this', 'param1', 'param2');

boundDoSomething();

调用 doSomething.bind 不会执行 doSomething ,它会返回一个包装了 'this', 'param1', 'param2'参数的另一个函数 -- boundDoSomething

调用 boundDoSomething 才会执行 doSomething,我们无需再传入参数,因为参数之前已经 bound 进去了。

另外,可以只 bind 头一部分

const boundDoSomething = doSomething.bind('this', 'param1'); // 只 bind param1
boundDoSomething('param2'); // 补上 param2

后一部分等调用时再补上也可以。

不过,前面 bound 了的,后面无法 override

const boundDoSomething = doSomething.bind('this', undefined, 'param2'); // 尝试 param1 留空
boundDoSomething('param3'); // 尝试补上 param1。不行!它将成为 param3

注:参数和 this 都无法 override。

 

class 的底层原理

上面我们提到,class 只是语法糖,它的本质是 Object + Function + Prototype。

在介绍 Object、Function、Prototype 时,我刻意避开了跟 class 有关的特性,因此它们都很好理解。

不过,一旦涉及 class 的特性,它们就会变得异常复杂。

这也是为什么 es6 会推出上层的 class 作为语法糖,用以隐藏这些复杂性。

作为 JS 的学习者,搞懂这些底层特性,有助于我们理解 JS 这门语言的灵活性。

虽然在日常业务代码中几乎不会用到,但在研究前端框架 (如 Angular) 的源码时,懂这些技巧还是很有帮助的。

我们就一起来探索吧🚀。

万物皆是对象

实例化一个日期

const date = new Date();

显然 Date 是一个 class,因为它可以实例化。

typeof Date 会是 ... ?

console.log(typeof Date); // 'function'

是函数😨

Object.keys 我们学过

const person = { name: 'Derrick', age: 11 }
console.log(Object.keys(person)); // ['name', 'age']

Objectkeys 方法,所以它是对象...吗?

console.log(typeof Object); // 'function'

不,它也是函数😨

其实是这样:ObjectDate 都是 class,而 typeof class'function'

const obj1 = {};
const obj2 = Object.create(Object.prototype);
const obj3 = new Object(); // Object 是 class,它可以实例化出对象

以上三种创建对象的方式完全是等价的。

结论:

  1. class 的本质是函数

    class Person {}
    console.log(typeof Person); // 'function'
  2. 函数也是一种对象

    function Person (){ }
    // Person 函数也是一种对象,所以它可以 add/remove property
    Person.age = 11;
    console.log(Person.age); // 11
    delete Person.age;
    console.log(Person.age); // undefined

new Function?

class 的本质是函数,难道我们 new 的是函数?

function Person () {}        // Person 是函数
const person = new Person(); // new 函数
console.log(typeof person);  // object
console.log(person instanceof Person); // true

没错!new 函数,会返回一个对象 (实例),即便函数没有 return。

我们可以把函数当作是 classconstructor

这个

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

const person = new Person('derrick', 11);

完全等价于这个

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

const person = new Person('derrick', 11);

有一个知识点 -- 函数中的 this

function Person (name, age) {
  console.log(this); // undefined;
}

const person = Person('derrick', 11);

如果是普通函数调用,那函数里的 this是 undefined。

如果是 new 函数,this 则是 person 实例。

static key (a.k.a static field) & block

这个

class Person {
  static age = 11;

  static {
    console.log('run');
  }
}

完全等价于

function Person() {}
Person.age = 11;
console.log('run');

因为函数也是对象,所以可以 set property for static key。

而 class block 只是一个在函数之后的执行而已。

private key

private key 是 es2022 才推出的。它不能简单的用函数去替代。

class Person {
  #age = 11;
}

底层实现是这样

let _Person_age, _Person_name;
function Person() {
  _Person_age.set(this, 11);
  _Person_name.set(this, 'derrick');
}
_Person_age = new WeakMap();
_Person_name = new WeakMap();

其实跟函数没什么关系了,主要是靠外部变量实现的。

define shared method

class Person {
  sharedMethod() {}
  nonSharedMethod = function() {}
}

const person = new Person();

console.log(person.sharedMethod === Person.prototype.sharedMethod);       // true
console.log(person.nonSharedMethod === Person.prototype.nonSharedMethod); // false

一个是 shared method,一个不是。

以函数来表达是这样

function Person() {
  this.nonSharedMethod = function() {}
}
Person.prototype.sharedMethod = function() {}

因为实例与 class 有 prototype 关系 (person.__proto__ 等于 Person.prototype),所以能透过原型链找到 sharedMethod 来调用。 

inherit 继承 (extends)

class 版本的继承

class Animal {
  constructor(age) {
    this.age = age;
  }
  jump() {
    console.log('jump', this.age);
  }
}
class Dog extends Animal {
  constructor() {
    super(11);
  }
  bite() {
    console.log('bite');
  }
}

const dog = new Dog();
dog.jump(); // 'jump' 11
dog.bite(); // bite
console.log(dog.__proto__ === Dog.prototype); // true
console.log(dog.__proto__.__proto__ === Animal.prototype); // true

函数版本的继承

function Animal(age) {
  this.age = age;
}
Animal.prototype.jump = function( ) {
  console.log('age', this.age);
}

function Dog () {
  Animal.call(this, 11);
}
Dog.prototype.bite = function() {
  console.log('bite');
}
Object.setPrototypeOf(Dog.prototype, Animal.prototype);

主要做两件事:

1. 调用父类构造函数

2. 串联 prototype (Dog to Animal)

class / function 总结

class 的所有特性,function 都有,因为 class 的本质就是 function

function Animal () {} // or class Animal {}
const dog = new Animal();

console.log(typeof Animal); // 'function'

console.log(Animal.prototype.constructor === Animal); // true

console.log(dog.__proto__ === Animal.prototype); // true 

console.log(Animal.prototype.__proto__ === Object.prototype); // true

console.log(dog instanceof Animal); // true
console.log(dog instanceof Object); // true

 

Mixins

参考:

Mixin Classes in TypeScript

TypeScript Docs – Mixins

YouTube – TypeScript Mixin Classes - JavaScript Multiple Class Inheritance // Advanced TypeScript

以前写过相关的文章 – angular2 学习笔记 (Typescript)

TypeScript Issue – Generating type definitions for mixin classes with protected members

多继承概念 (组合 class)

我们常听人家说,多用组合,少用继承,其原因是继承不够灵活。

某一些语言是有支持多继承概念的 (比如 C++),但许多语言是没有支持的 (比如 C# / Java)。

多继承就是一个 class 同时 extends 好几个 class,也可以理解为把多个 class 组合起来。

类似这样

class ParentA {}
class ParentB {}
class ChildA extends ParentA, ParentB {} 

这样 ChildA 就同时有了 ParentAParentB 的属性。

真实使用场景

查看 Angular Material 源码 (以前的,现在没有了) 会发现,里面大量使用了多继承的概念 (也可以理解为组合 class)

这些 mixinXXX 的方法里头都封装着一个 class

  

很明显,多继承肯定比单继承灵活,至于是否增加了复杂度,这个不一定,但有你可以选择不用,总好过没有。

JavaScript 不支持多继承

JS 没有支持多继承,但是!

JS 灵活啊,它总有它自己的办法去实现的,就像用 Function 来实现 class 那样。

动态继承 == 多继承

JS 是动态语言,因此它可以动态创建 class 和 extends,这就让它有了实现多继承的能力。

首先,来个需求

class ParentA {
  parentA = 'a';
}
class ChildA extends ParentA{}

class ParentB {
  parentB = 'b';
}
class ChildB extends ParentB{}

假设,我想封装 ChildAChildB 的共同属性,那么我就做一个 ChildAB

如果 JS 支持多继承, 那么它大概长这样

class ParentA {
  parentA = 'a';
}
class ChildA extends ChildAB, ParentA {}

class ParentB {
  parentB = 'b';
}
class ChildB extends ChildAB, ParentB{}

class ChildAB {
  childABValue = 'c';
}

JS 要实现要靠动态继承

首先把 ChildAB 改成一个方法

function mixinsChildAB(baseClass) {
  return class extends baseClass {
    childABValue = 'c';
  };
}

看到吗,baseClass 是参数,因此它是动态 create class + variable extends

接着

const ChildABToParentA = mixinsChildAB(ParentA);
class ChildA extends ChildABToParentA {}

现在的关系是 ChildA ChildABToParentA ≼ ParentA

接着

const ChildABToParentB = mixinsChildAB(ParentB);
class ChildB extends ChildABToParentB {}

在创建一个 class 链接到 ParentB

现在的关系是 ChildB ChildABToParentBParentB

最后

const childA = new ChildA();
console.log(childA.childABValue); // c
console.log(childA.parentA); // a

const childB = new ChildB();
console.log(childB.childABValue); // c
console.log(childB.parentB); // b

总结

JS 没有多继承,它只是利用动态创建 class extends的特性,实现了动态继承。

从而间接达到了类似多继承的效果。

 

posted @ 2022-05-08 14:05  兴杰  阅读(638)  评论(0)    收藏  举报