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

前言

JavaScript (简称 JS) 有几个概念 Object, Prototype, This, Function, Class 是比较难理解的 (相对其它语言 C# / Java 而已),这主要是因为 JS 设计之初并没有完善这几个部分 (当时没有需求),

而后来一点一点补上去的时候又需要考虑向后兼容,于是就造就了各种奇葩现象,最终苦了学习者。

如果你正被这些概念困扰着,想要理清它们,那你就来对地方了,本篇会让你理清它们之前错综复杂的关系。

 

Function

JS 是一门极其灵活的语言,它可以面向对象编程 (Object-Oriented Programming,OOP),同时也可以函数式编程 (Functional Programming)。

你在 JS 会看见很多巧妙的特性,既满足了 OOP 又满足了 Functional。

我们就先从 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'));

函数出没的地方

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

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

可以 assign 给变量

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

可以当参数传

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

可以当函数返回值

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

宽松的参数数量

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

举例,函数声明了一个参数,但是调用时传入超过 1 个参数是 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); // log 'default value'
myFunction1();          // log 'default value'
myFunction1('value');   // log 'value'

用等于设置 defualt 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

总结

以上就是函数的基础知识。如果你觉得很简单,那就对了,因为我刻意避开了容易操作混乱的特性,我们在下面 right time 时才补上。

好,我们去下一 part🚀。

 

Object

JS 的 Object 又称 "对象" 指的就是面向对象编程 (Object-Oriented Programming,OOP) 里的 "对象“。

但有别于纯正的 OOP 语言 (C# / Java),JS 也支持 Functional,再加上 JS 还是动态类型语言,

这就导致了我们无法直接用 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. alias

    纯正的 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,存在就是 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 只代表 1 个 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 可以。

    反过来 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 的功能。

    之后这个 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 遍历出来,

    而 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 = {} 或者 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 本来已存在,那 defineProperty 如果没有额外声明 configurable, enumerable, writable 那它会保持原来的,如果有声明则会 override。

批量设置 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 的基础知识。如果你觉得很简单,那就对了,因为我刻意避开了容易操作混乱的特性,我们在下面 right time 时才补上。

好,我们去下一 part🚀。

 

Classes

有对象,有函数,其实已经可以做到 OOP 了。但是对比其它纯正 OOP 语言 (C#, Java) JS 还少了一个重要的特性 -- Classes。

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

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

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

因为早在 es6 之前,其实 JS 已经能实现 class 特性了,只不过它是利用 Object + Function + Prototype 这 3 种特性,间接实现了 class 特性。

这种间接的方式有许多问题:难封装,难理解,代码又碎。最后随着 JS 越来越多人用,越来越多人骂,为了平息众怒,ECMA 只能另外推出语法糖 class。

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

我们先搞清楚 JS 上层的 class 语法糖,之后再去看底层 Object + Function + Prototype 是如何实现 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 添加就是了。

  2. outside constructor

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

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

    我们通常不会使用 ouside define 属性,因为它没有什么意义。

3. define method

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

上面我们有提到,constructor 里的 this 指向新实例,这种函数中使用 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'

getter, setter, sayMyName 这三个方法里的 this 都指向 person 对象。

另外,普通函数里的 this 则指向 window 对象 (游览器环境下)。

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

提醒:通常只有在对象方法和 class constructor 里,我们才会使用到 this,普通函数里是不会使用 this 的,所以我们最好不要把它掺和进来捣乱,把它忘了呗。

好,回到主题 -- 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 还有 2 个大的区别:

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

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

        fullName 和 sayMyName 都不在 key list 里。

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

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

        而 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

        不同实例但是方法指针是同一个。如果是 inside define 那每一个实例的方法都是不同的指针。

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

        如果 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 如何让所以实例共享一个方法指针的,我们下面会再讲解。

  3. outside define but use old school syntax

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

    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 (but don't use 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 key value 任然是可以的。

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

define private key

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

  method() {
    console.log(this.#privateValue);
  }
}

const person = new Person();
console.log(Object.keys(person));  // [] empty array
person.method(); // 'value'
console.log(person.#privateValue); // 报错!SyntaxError: 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. 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 还会顾虑继承的情况,细节下面会再讲解。

 

TODO KEAT 要继续修

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() {}
  sameNameMethod() { // override Parent method
    // do something decorate
    super.sameNameMethod();
    // 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

继承有几个特性

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

2. new Child 时, Parent 的 constructor 会被调用

3. child 实例即算是 Child 的实例同时也算是 Parent 的实例 (instanceof)

4. 但它只有一个 constructor 那就是 Child.

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

注意: 一旦 override 了, 即便是 Parent 也会调用到 override 的哦, C# 有一种 new keywords 可以做到只有 Child 调用 new method, Parent 依然调用旧的, 这个不属于 override. 但 JS 没有这个特性, JS 一定是 override 的.

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.

 

 

 

TODO KEAT 完成了的

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

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

整条原型链是:person -> Object.prototype -> null 一共 2 个可查找的对象。

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

当调用 person.toString 时,JS 引擎先找 person 的 keys 看有没有 toString 方法,结果没有,于是就去找 person 的 Prototype 也就是 Object.prototype 对象,

然后就找到了 toString 这个方法。而 toNumber 方法没有在 Object.prototype 中,所以它会报错。

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' }); 

用 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, entries, getOwnPropertyNames 都无法遍历出 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 in 和 Object.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 实际上是调用了 parent.sayMyName,但是 sayMyName 里的 this 指向的却是 child 而非 parent。

在 Prototype 的影响下,this 指针被偷龙转风了。

总结

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

就两个概念:

  1. 对象和对象关联起来后叫原型链 (你要叫对象串也可以)

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

Prototype 对 Object 基础知识没有什么破坏,算是一个扩展,so far 还没有上难度,我们继续下一 part 吧🚀。

 

 

 

 

 

 

TODO KEAT 要加进去 

箭头函数 (es6)

参考: 阮一峰 – 箭头函数

箭头函数是函数的缩减版本,有点像 C# 的 lambda,它常被用来替代匿名函数,比如当参数传或者作为函数的返回值。

它的特色就是简单,看上去干净。

// es5
const values2 = [1, 2, 3].filter(function (v) {
  return v > 1;
});  

// es6
const values1 = [1, 2, 3].filter(v => v > 1);  

箭头函数内没有 arguments 对象,如果想拿到所有参数可以使用 rest parameters

function doSomething(callbackFn) {
  callbackFn('a', 'b', 'c')
}
doSomething((...args) => console.log(args)); // ['a', 'b', 'c']

另外,箭头函数内也没有属于自己的 this,它的 this 有点像闭包,依据上下文决定

function doSomething(callbackFn) {
  callbackFn('a', 'b', 'c')
}

const person = {
  sayMyName() {
    console.log(this === person); // 2. 就是外面的 this
    doSomething((...args) => {
      console.log(this === person);  // 1. 里面的 this
      console.log(args)
    }); 
  }
}
person.sayMyName();

console.log(this === window); // 4. 就是外面的 this doSomething((...args) => { console.log(this === window); // 3. 里面的 this console.log(args) });

 

 

TODO KEAT 要加进去

对象箭头函数的 this

const person = {
  firstName : 'Derrick',

  sayName: () => {
    console.log(this.firstName);  // undefined
    console.log(this === window); // true
  }
}
console.log(this === window);

person.sayName();

对象方法使用箭头函数是很罕见的,箭头函数的特色是它没有自身的 this。

上面例子中,sayName 里的 this 不指向对象,而是 window,因为它和最外头的 this 其实是同一个。

 

 

  

  

 

 

 

 

 

 TODO KEAT

 

前言

JavaScript 有几个概念 Object、Class、This、Prototype、Function 比较难理解 (相对其它语言 C# / Java 而言),这主要是因为 JS 早期的定位和后期发生的巨变。

而改进语言又需要向后兼容,所以就造成了很多混乱。这一篇就是要讲清楚它的来龙去脉。

 

Simple Object

从最简单的对象操作开始,慢慢才加入其它概念 (由浅入深)。

create, get, set, add, delete

// create object
const person = {
  name: 'Derrick',
  age: 11,
};
console.log(person.name); // get property
person.name = 'Alex'; // set property
person.salary = 100; // add property
delete person.salary; // delete property

JS 对象的属性是可以动态添加和删除的。

遍历 keys

const allKeys = Object.keys(person);
for (const [key, value] of Object.entries(person)) { // es2017
  console.log(key, value); // name Derrick ... age 11
}

两个知识点:

  1. Object.keys,Object.entries 只能遍历 own property 和 enumable property。

    不清楚怎样才算 own 和 enumable 的话,下面会讲解。

  2. 如果 key 是 symbol,那是遍历不出来的,需要使用 Object.getOwnPropertySymbols 方法。

    const key = Symbol();
    const person = {
      [key]: 'value',
    };
    console.log(person[key]); // 'value'
    console.log(Object.keys(person)); // [] empty array
    console.log(Object.getOwnPropertySymbols(person)[0] === key); // true

define method

const person = {
  method1: function () {},
  method2() {},
  method3: () => {}
};
person.method1();

method1,method2 的写法是完全等价的,建议用 method2 的写法。
method 3 则不同,最大的区别是方法内 this 的指针,下面会教。

getter setter

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

defineProperty

JS 的属性可以配置一些 config,看注释理解

Object.defineProperty(person, 'name', {
  configurable: true, // 执行完这一次 config 后是否允许再设置 config (默认是 true)
  writable: true, // 属性可以被赋值吗? (默认是 true), 如果 false 那么在执行 person.name = 'new value' 会被无视 (不会报错哦)
  enumerable: true, // 是否可以被 Object.keys, Object.entries 遍历出来 (默认是 true), 设置 false 可以通过 getOwnPropertyNames 遍历所有 keys
  value: 'Derrick',
});

Object.defineProperty(person, 'fullName', {
  configurable: true,
  enumerable: true,
  // 也可以 define getter setter
  get() {
    return this.firstName + ' ' + this.lastName;
  },
});

也可以读取 config 查看

const person = {
  name: 'Derrick',
};
const config = Object.getOwnPropertyDescriptor(person, 'name');
console.log(config);

效果

默认全部都是 true

defineProperties getter setter

上面的 fullName getter setter 利用了 public 属性 firstName, lastName, 但大部分场景, 我们需要一个 "private" 属性.

const person = {};
Object.defineProperties(person, {
  _privateValue: {
    value: 'default value',
  },
  publicValue: {
    get() {
      return this._privateValue;
    },
    set(value) {
      this._privateValue = value;
    },
    enumerable: true,
  },
});
console.log(Object.keys(person)); // ['publicValue']

这里有三个点要注意

1. defineProperties 可以批量定义 config. 但是它的 default 默认值和 defineProperty 相反, enumerable, writable, configurable 默认都是 false (defineProperty 则全部是 true). 挺奇葩的...

2. JS 的 private property 只是无法被 enumerable 而已, 直接访问 person._privateValue 依然是可以访问到的. 虽然不足, 但足够应付大部分场景了.

p.s. ES2022 引入了真正 private property 概念, 语法是 starts with hash, 但只能用在 class 里

class Person {
  #privateValue = 'value';
}

若想在早版本实现, 那可以用 WeakMap (参考 TypeScript 实现)

var _Person_privateValue;
class Person {
    constructor() {
        _Person_privateValue.set(this, 'value');
    }
}
_Person_privateValue = new WeakMap();

3. get set 和 writable, value 是相互冲突的, 设定其其中一边, 另一边就无法设置了.

 

Class

class 是 es6 的语法糖, 它底层是依靠 es5 许多特性来实现的. 下面我会一一介绍. 我们先从简单容易了解的上层 Class 切入.

class Person {
  // 每次 new Person(), constructor 都会被调用
  constructor(name, age) {
    // this 指向每一次 new 出来的新对象
    this.name = name; // add property to new instance
    this.age = age; 
  }
  salary = 1000; // 等价于 constructor 内写 this.salary = 1000;

  method() {}

  // es2022
  #privateValue = 'default';
  get value() {
    return this.#privateValue;
  }
  set value(value) {
    this.#privateValue = value;
  }
}
const person = new Person('Derrick', 100);

Class 是对象的模板, 或者说工厂. new Class() 表示创建一个对象 (又叫实例化)

和上面 simple object 的创建方式相比, Class 的方式更有结构, 对管理加分, 这是它最重要的 point.

simple object 的版本如下 (它们其实有一些微妙的区别哦)

const person1 = {
  name: 'Derrick',
  age: 100,
  salary: 1000,
  method() {},
};
Object.defineProperties(person1, {
  _privateValue: {
    value: 'default',
  },
  value: {
    get() {
      return this._privateValue;
    },
    set(value) {
      this._privateValue = value;
    },
    enumerable: true,
  },
});
View Code

历史原因

JS 诞生之初其实 Class 的概念在其它语言早就有了 (比如 Java), 只是 JS 要解决的场景比较简单, 所以设计者才放弃了 Class 的语法. 

它设计者并没有完全封死 Class 的概念, 它利用了其它的语言特性, 让 JS 在没有 Class 的情况下也可以实现 Class 的特性. 

而事实证明它的预期是对的. 前期 JS 用 simple object 就满足了需求, 不需要 Class 的复杂性. 偶尔需要的时候也可以用其它特性去模拟 Class 效果.

一直到后来 JS 发展开来, 处理的场景愈加复杂 es6 才引入 Class 语法糖, 之所以它是语法糖是因为 JS 本来就具备实现 Class 特性的能力. 所以不需要去搞多一套, 只是上层封装就好了.

当然这也苦了 JSer, 需要去理解这中间的曲折和不直观.

Class 的特性

1. constructor (构造函数)

constructor 的职责是初始化对象属性值, 每一次 new Class() constructor 都会被调用.

然后返回一个新对象 (虽然 constructor 并没有写 return).

函数里面的 this 指向当前对象.

所以 this.name = name; 就是往这个新对象 add property 的意思.

p.s.constructor 其实是可以写 return 的, return 值必须是对象. 然后这个对象就会变成 new 返回的对象, 但最好不要搞这些冷门招数啦. 很乱水的.

2. private field

class Person {
  // es2022
  #privateValue = 'default';
  get value() {
    return this.#privateValue;
  }
  set value(value) {
    this.#privateValue = value;
  }
}

这个 private field 无法通过 defineProperty 实现, es2022 之前需要用到全局 WeakMap 才能实现. 

3. 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

静态属性/方法不需要 new Class, 它是直接调用的 Class.staticField (e.g. Person.publicName)

静态属性也可以通过 # 变成 private static field

static block 是一个立即执行作用域. 注意看上面的 log('before class Person done') 执行会早于 log('class Person done')

4. shared method

class Person {
  constructor() {
    this.method1 = function () {};
  }
  method2 = function () {}; // 等价于在 constructor 写 this.method2 = function () {};

  method3() {
    console.log(this.name);
  }
}
const person1 = new Person();
const person2 = new Person();
console.log(person1.method1 === person2.method1); // false
console.log(person1.method2 === person2.method2); // false
console.log(person1.method3 === person2.method3); // true

method3 的定义会使得它变成 shared method, 所有实例的 method3 都是同一个 reference. 

而 method1 则没有 share 的概念, 每一个实例都有自己的 method1

method2 的写法其实和 method1 是完全等价的, 所以也没有 share

5. instanceof 和 constructor

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

通过 instanceof 我们可以判断某个对象是否来自某个 Class

也可以通过 .constructor 获取它的对象的 Class

注: instanceof 并不是通过 constructor 值来做判断的哦, 它是通过 prototype, 下面会讲到.

6. 构造函数和方法中的 this

class Person2 {
  constructor(name) {
    this.name = name;
  }
  method() {
    console.log(this.name); // Derrick
  }
}
const person2 = new Person2('Derrick');

在没有任何奇葩操作情况下, constructor 和 method 中的 this 指向 new instance

多一个例子

onreadystatechange 是 xhttp 对象的 method, method 中的 this 指向的是 xhttp 对象.

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() {}
  sameNameMethod() { // override Parent method
    // do something decorate
    super.sameNameMethod();
    // 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

继承有几个特性

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

2. new Child 时, Parent 的 constructor 会被调用

3. child 实例即算是 Child 的实例同时也算是 Parent 的实例 (instanceof)

4. 但它只有一个 constructor 那就是 Child.

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

注意: 一旦 override 了, 即便是 Parent 也会调用到 override 的哦, C# 有一种 new keywords 可以做到只有 Child 调用 new method, Parent 依然调用旧的, 这个不属于 override. 但 JS 没有这个特性, JS 一定是 override 的.

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.

 

术语

这里穿插一段术语讲解. 不然会有一点点乱水.

Object 是对象

Class 是类

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

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

Property 是属性, Method 是方法, Key 很抽象, 在 C# 对象中, 当 value = 函数时, 它的 key 被称为 method, 非函数时, key 被称为 property

Constructor 是 Class 的初始化函数

它们继承关系是这样:

Instance ≼ Object

Property/Method ≼ Key

Constructor ≼ Function

很多时候不那么讲究, 也可以把它们当同义词看

 

Prototype

prototype 原型链是 JS 的一个特色. 它可以实现对象继承功能. 我们先不要把它和 Class inherit 联想到一起. 

我们就单纯看这个 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 的值? 

原型链查找

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

这个 __proto__ 指的就是 prototype.

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

如果找到了就会返回它的值, 如果找不到就继续找, 因为 __proto__ 也是对象, 所以它也有 __proto__, 一直找到 __proto__ = null 结束.

Object.prototype

const person = {};
console.log(person.__proto__ === Object.prototype); // true
console.log(Object.getPrototypeOf(person) === Object.prototype); // 正确获取 prototype 的方式是调用 Object.getPrototypeOf

当我们创建对象时, 对象的默认 prototype 就是 Object.prototype 对象. 而这个对象中就包含了许多方法, 比如 toString. 所以上面调用 person.toString 没有报错.

当调用 person.toString() 时, JS 引擎先找 person 的 keys 看有没有 toString 方法, 结果没有, 于是就去找 person prototype 也就是 Object.prototype 这个对象.

然后就找到了 toString 这个方法. 而 toNumber 并没有在 Object.prototype 中, 于是就报错了.

原型链的终点

既然 Object.prototype 也是对象, 那它也有 __proto__ 吗? 其实是没有的.

console.log(Object.prototype.__proto__); // null

通常 Object.prototype 会作为原型链的结尾. 

修改 prototype

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

创建对象同时指定 prototype

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

通过 Object.create 可以在创建对象的同时指定其 prototype.

用 prototype 特性实现 inherit

inherit 的特色是 child 可以 call parent 的方法. 这就可以利用原型链机制实现

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

不只是方法, 属性也是可以的, 但是通常不鼓励用原型链来搞属性, 因为...

小心坑

看注释

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 而是 new 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, 而不是 new property to child 哦 (和上面不同)
console.log(child.fullName.firstName); // 注意: still read from parent

以前 AngularJS 的 $scope 就使用了 prototype 概念, 导致了许多人在赋值的时候经常出现 bug. 原因就是上面这样. 

你 get from prototype 不代表就是 set to prototype, 因为属性是可以动态添加的. 你以为是 set, 结果变成了 add property.

遍历原型链所有属性

上面介绍的 Object.keys, Object.entries 都只能遍历当前对象的属性. 

for...in 除了可以遍历对象属性还可以遍历出所有 prototype 链上的属性 (属性必须是 enumerable: true)

for (const key in obj) {
  const isFromPrototype = !obj.hasOwnProperty(key); // 判断是当前对象还是 prototype 属性
}

 

Function as Class

上面我们讲的 Object, Class, Function, Prototype 都比较直观, 那是因为我刻意 skip 掉了混乱的 part. 

现在来一个混乱的. 上面提到 class 只是语法糖, 它的本质是 Function. 

JS 的 Function 就是那么独特, 可以当 class 来用... 就问你乱不乱?

上面我们提到 class 有 6 个特性, 现在我们来对应它的 Function 实现

1. constructor (构造函数)

class Person2 {
  constructor(name) {
    this.name = name;
  }
}
const person2 = new Person2('Derrick');

function Person1(name) {
  this.name = name;
}
const person1 = new Person1('Derrick');

仔细看 class 的 constructor 不就是一个函数嘛... 只是内部多了一个 this 指向实例的概念.

还有 new Class 等于创建新对象的意思.

于是 JS 的 Function 就多了这 2 个特性

1. 可以 new Function 表示输出一个对象

2. 函数内部可以调用 this, 它指向新实例

2. private field

这个是 class 独有的. Function 并没有这个功能. 参考 TypeScript Transpile

es2022

class Person {
  #privateValue = 'value';
}

es2021

var _Person_privateValue;
class Person {
    constructor() {
        _Person_privateValue.set(this, 'value');
    }
}
_Person_privateValue = new WeakMap();

需要用全局 + WeakMap 来实现

3. static field & block

JS 的函数也是对象. 所以它可以有属性.

static filed 就只是函数的属性而已

function Person() {}
Person.publicName = 'public';
(() => {
  console.log('before class Person done');
})()

static block 也只是紧跟着函数后面的自执行函数而已.

至于 private static field 就不同, 因为只有 class 才有 private, 函数没有这个概念.

参考: TypeScript 的 transpile 是这样的

4. shared method

要让 2 个实例用同一个函数指针, 就需要利用 prototype 特性.

const person1 = {};
const person2 = Object.create(Object.prototype);
const person3 = new Object();

这 3 个写法是等价的, 1, 2 写法上面讲过了, 现在我们关注第 3 种 new Object()

首先 typeof Object 会发现它是一个 Function

console.log(typeof Object); // function

类似于这样

function Object() {}

这也对应了我们讲的 JS Fuction 是可以 new 的, 然后它会返回一个实例.

另一个发现是 new Object 出来的对象, 它的 __proto__ 会指向 Object.prototype, 这就是 JS 的机制.

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

类似于

function Object() {}
Object.prototype = {
  toString: function () {},
};

依照这个原理, 我们试试做一个函数. 看看是不是也一样机制

function MyObject() {}
MyObject.prototype = {
  myToString: function () {
    console.log('call');
  },
};
const person = new MyObject();
console.log(Object.getPrototypeOf(person) === MyObject.prototype); // true
person.myToString(); // call

完全正确.

依据原型链查找机制, person 本身没有 myToString 方法, 它其实是找到了 MyObject.prototype 里的 myToStrong 方法, 所以

const person1 = new MyObject();
const person2 = new MyObject();
console.log(person1.myToString === person2.myToString);

person1 和 person2 的 myToString 其实都是 MyObject.prototype.myToString. 这就实现了 shared method.

5. instanceof 和 constructor

instanceof 的判断机制

instance instanceof MyClass
相等于
instance instanceof MyFunction

它的判断过程是, instance.__proto__ 是否等于 MyFunction.prototype. 如果不等于那么就继续尝试上一个原型.

instance.__proto__.__proto__ 是否等于 MyFunction.prototype. 一直到 __proto__ = null 结束.

例子证明

function ParentClass() {}
ParentClass.prototype = {};

function ChildClass() {}
ChildClass.prototype = Object.create(ParentClass.prototype);

const person = Object.create(ChildClass.prototype);
console.log(person instanceof ChildClass); // true, 因为 person.__proto__ === ChildClass.prototype
console.log(person.__proto__ === ChildClass.prototype);
console.log(person instanceof ParentClass); // true, 因为 person.__proto__.__proto__ === ParentClass.prototype
console.log(person.__proto__.__proto__ === ParentClass.prototype);

constructor 机制

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

首先, person 是空对象, 没有 key. 但是有 constructor 属性, 这不免让人怀疑 constructor 来自 __proto__

事实证明确实如此

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

所以 JS 的机制是, 当我们声明一个 Function 时, Function.prototype 就自动创建了, 而且里面还有属性 constructor 属性, 值就是当前函数 (例子中就是 function Person).

如果 override prototype, 那记得手动设置回 constructor.

function Person() {}
Person.prototype = {};
const person = new Person();
console.log(person.constructor === Person); // false;
console.log(person.constructor === Object); // true;

person.constructor 本来应该要访问到 person.__proto__.constructor 但结果变成了 person.__proto__.__proto__.constructor.

正确 override 方式

function Person() {}
const prototype = {};
Object.defineProperty(prototype, 'constructor', {
  value: Person,
  enumerable: false,
});
Person.prototype = prototype;
const person = new Person();
console.log(person.constructor === Person); // true;

6. 构造函数和方法中的 this

上面我们说到, class 的 constructor 和 method 中的 this 都指向实例. 这个其实是被简化过了的 (让JS 和 C#, Java 行为一致)

但其实 JS 函数中的 this 是相当混乱的.

比如

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

当一个函数没有被 new 调用, 而里面又使用了 this, 那这时 this 指向 window.

当然我们不应该让这种混乱诞生. best practice 就是, 要在函数内使用 this, 那么这个函数就应该被当成一个 Class 来使用. 一定要 new.

this 的偷龙转风

function Person() {
  this.name = 'Derrick';
}
Person.prototype.method = function () {
  console.log(this.name);
};

const person = new Person();
person.method(); // 'Derrick'

const person2 = { name: 'Alex' };
person2.method = person.method; // 偷龙转风
person2.method(); // 'Alex'

person.method.call({ name: 'Alex' }); // Alex 偷龙转风

函数中 this 其实是不固定的, 调用它的人有权利控制它指向哪一个对象.

当 person2.method = person.method 后, 虽然 method 是同一个 reference 但是它 belong to 的对象已经不同了. 所以 this.name 也就不同了.

person.method.call 更极端, 它可以指定任何对象作为 method 中的 this.

箭头函数的 this

this 应该被用在 Function as Class 的场景, 而箭头函数是不可以 new 的, 它完全无法作为 Class.

它内部的 this 其实指向的是外部 scope 的 this.

function Person() {
  this.name = 'Derrick';
  this.method = () => {
    console.log(this.name);
  };
  this.method2 = function () {
    console.log(this.name);
  };
}
const person = new Person();
person.method.call({ name: 'Alex' }); // Derrick
person.method2.call({ name: 'Alex' }); // Alex

箭头内的 this 没有被偷龙转风. 

因为它就像是使用了闭包来传递 this.

function Person() {
  this.name = 'Derrick';
  this.method = () => {
    console.log(this.name);
  };
  // 等价于
  this.name = 'Derrick';
  const _this = this;
  this.method = () => {
    console.log(_this.name);
  };
}

7. inherit 继承 (extends)

参考: js实现ts中类的继承

继承有两件事要做到

1. parent 构造函数调用

2. shared method

3. static fileds 

function Person(name) {
  this.name = name;
}
Person.prototype.personMethod = function () {};

function Alex(name, age) {
  Person.call(this, name); // for property
  this.age = age;
}
Alex.prototype.alexMethod = function () {};
Object.setPrototypeOf(Alex, Person); // for static fields
Object.setPrototypeOf(Alex.prototype, Person.prototype); // for methods (shared)

parent 构造函数是利用了 this 偷龙转风的机制实现的. 这样所有属性都会被 add 到当前实例里面.

而方法则是通过 prototype 继承的方式去实现. 同时它也满足了 instanceof.

// Alex.prototype > Object.prototype
// 变成
// Alex.prototype > Person.prototype > Object.prototype
Object.setPrototypeOf(Alex.prototype, Person.prototype); 

还有, super 调用父类方法是用 this 偷龙转风做到的

Alex.prototype.alexMethod = function () {
  // super.personMethod();
  Person.prototype.personMethod.call(this);
};

 

小总结

所有混乱都来自于 JS 一开始的定位. 它选择了不直接实现 Class 特性. 反而是通过各种其它特性去实现 Class.

早年问题没有曝露就是因为用 Class 的需求少. 而后来只能通过 es6 来引入语法糖, 毕竟其它特性已经可以实现了 Class 了 (虽然不是 100% 和其它语言一样). 总不会去搞 2 套.

这年头其实对上面这些混乱一知半解是 ok 的, 只要你 follow best practice, 不要自作聪明, 写代码一样是不会出 bug 的.

当然如果你用了那些自作聪明的 framework 你还是会出 bug, 比如多年前的 AngularJS. 但后来的 Angular 就没有这些问题了.

 

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 就同时有了 ParentA 和 ParentB 的属性.

真实使用场景

查看 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{}

假设, 我想封装 ChildA 和 ChildB 的共同属性. 那么我就做一个 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  ≼ ChildABToParentB ≼ ParentB

最后

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

 

总结

上面就是 Function 的基础知识。Function 和 Object 的关系不大,

唯一的关系是,对象的 key value 可以是函数,而此时函数内的 this 将指向对象,仅此而已。

Function 和 Prototype 则几乎没有关系。

好,目前为止 Object、Prototype、Function 它们的知识都还相互独立,没什么难度,我们继续下一 part 吧🚀。

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

    Object.defineProperty(this, 'fullName', {
      get() {
        return this.firstName + ' ' + this.lastName;
      },
    });
   
    this.sayMyName = function() {
      console.log(this.fullName);
    }
  }
}
posted @ 2022-05-08 14:05  兴杰  阅读(489)  评论(0编辑  收藏  举报