前端工作室专注前端开发
 博客首页

《ES6标准入门》(第3版作者:阮一峰)读书笔记 19-26章

2021-03-16 10:04:39
JS基础与深入
16
文章图片

对国内十分有名的一本书--《ES6标准入门》进行个人的读书总结,对书中的知识点进行实践验证和记录。想要看原版的请购买正版书籍。

《ES6标准入门》(第3版作者:阮一峰)读书笔记 19-26章

第19章 Class的基本语法

19.1 简介

传统方法创建新对象

function Person(name,age){
    this.name = name;
    this.age = age;
}
Person.prototype.personDetail = function (){
    console.log(this.name,this.age)
}
var person = new Person('lee',18)

通过Class改写

class Person{
    constructor(name,age){
        this.name = name;
        this.age = age;
    }
    personDetail(){
        console.log(this.name,this.age)
    }
}
typeof Person // function(看作是构造函数的另一种写法)
Person == Person.prototype.constructor // true
// 使用new创建
var person = new Person('lee',18)
console.log(person) // Person {name: "lee", age: 18}

类的方法全部都定义再类的prototype属性上

console.log(person.__proto__) // {constructor: ƒ, personDetail: ƒ}

所以

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

相当于

Person.prototype = {
    construct(){},
    personDetail(){}
}

类内部的所有方法都是不可枚举的

Object.keys(Person.prototype) // []
Object.getOwnPropertyNames(Person.prototype) // ["constructor", "personDetail"]

ES5写法时类内部方法是可以枚举的

function Person(name,age){
    this.name = name;
    this.age = age;
}
Person.prototype.personDetail = function (){
    console.log(this.name,this.age)
}
var person = new Person('lee',18)
console.log(Object.keys(Person.prototype)) // ["personDetail"](可以枚举)
console.log(Object.getOwnPropertyNames(Person.prototype)) // ["constructor", "personDetail"]

类属性名可以采用表达式

let methodName = 'personDetail'
class Person{
    constructor(name,age){
        this.name = name;
        this.age = age;
    }
    [methodName](){
        console.log(this.name,this.age)
    }
}

19.2 严格模式

类和模块内部默认使用严格模式

19.3 constructor方法

constructor方法是类的默认方法,通过new命令生成对象实例时自动调用该方法。如果没有显式定义,一个空的constructor方法会被自动添加。

class Person{}
console.log(Person.prototype) // {constructor: ƒ}(自动添加了)

constructor指定另一个返回对象

class Person{
    constructor(){
        return {}
    }
}
new Person() instanceof Person; // false(导致判断出错,所以为什么还要改它?)

当作函数直接调用报错

class Person{

}
Person() // 报错 Uncaught TypeError: Class constructor Person cannot be invoked without 'new'

19.4 类的实例对象

实例属性除非显示定义在其本身即this对象上,否则都是定义在原型上。

class Person{
    constructor(name,age){
        this.name = name;
        this.age = age;
    }
    personDetail(){
        console.log(this.name,this.age)
    }
}
var person = new Person('lee',18);
person.personDetail(); // lee 18
console.log(person.hasOwnProperty('name')) // true(在自身上)
console.log(person.hasOwnProperty('age')) // true(在自身上)
console.log(person.hasOwnProperty('personDetail')) // false(不在自身上)
console.log(person.__proto__.hasOwnProperty('personDetail')) // true(在原型上)

所有实例共享一个原型对象

var person1 = new Person('my',18);
var person2 = new Person('mht',18);
person1.__proto__ === person2.__proto__; // true

使用__proto__修改原型链时,所有实例都会被影响。

person1.__proto__.personDetail = function(){
    console.log('我要修改全部原型')
}
person1.personDetail(); // 我要修改全部原型
person2.personDetail();// 我要修改全部原型

19.5 Class表达式

Class使用表达式形式

const Man = class Person{
    getClassName(){
        return Person.name;
    }
}
let man1 = new Man();
man1.getClassName(); // "Person"
Person.name // 报错 Uncaught ReferenceError: Person is not defined(已经不能在外部使用)

Person只在Class内部代码可用,指代当前类 如果内部也没有用到那么可以省略Person

const Man = class{
    getClassName(){
        return this.name;
    }
}
let man1 = new Man();

立即执行的Class

const Man = class{
    constructor(name){
        this.name =name;
    }
}
let man1 = new Man('lee');
console.log(man1) // Man {name: "lee"}

19.6 不存在变量提升

new Person();
class Person{

} // Uncaught ReferenceError: Cannot access 'Person' before initialization

19.7 私有方法

加下划线标记私有方法

class Person {
    //公有方法
    getName(){}
    // 私有方法
    _getAge(){}
}

将私有方法移出模块

class Person {
    _setAge(age){
        setAge.call(this,age) // 借用call
    }
}
function setAge(age){ // 这是私有方法,,必须通过公有方法访问
    return this.age = age
}
var person = new Person();
person._setAge(18);
console.log(person) // Person {age: 18}

利用Symbol值的唯一性将私有方法的名字命名为Symbol值

const bar = Symbol('bar');
const snaf = Symbol('snaf')
export default class myClass{
    // 公有方法
    foo(baz){
        this[bar](baz)
    }
    // 私有方法
    [bar](baz){ // 必须通过上一个方法调用
        return this[snaf] = baz;
    }
}

19.8 私有属性

19.9 this的指向

如果单独提取出来单独使用,this会指向方法运行时的所在环境

class Logger{
    printName(name = 'lee'){
        this.print(`hi ${name}`)
    }
    print(text){
        console.log(text)
    }
}
const logger = new Logger();
logger.printName(); // hi lee(这样调用没问题)
const {printName} = logger // 取出来调用报错
printName(); // Uncaught TypeError: Cannot read property 'print' of undefined

修复一

class Logger{
    constructor(){
        this.printName = this.printName.bind(this) // 重新指定this指向
    }
    printName(name = 'lee'){
        this.print(`hi ${name}`)
    }
    print(text){
        console.log(text)
    }
}
const logger = new Logger();
const {printName} = logger
printName(); // hi lee(没问题了)

修复二

class Logger{
    printName = (name = 'lee')=>{
        this.print(`hi ${name}`)
    }
    print(text){
        console.log(text)
    }
}
const logger = new Logger();
const {printName} = logger
printName(); // hi lee

使用Proxy,获取式自动绑定this

class Logger{
    printName = (name = 'lee')=>{
        this.print(`hi ${name}`)
    }
    print(text){
        console.log(text)
    }
}
function selfish (target) {
  const cache = new WeakMap();
  const handler = {
    get (target, key) {
      const value = Reflect.get(target, key);
      if (typeof value !== 'function') {
        return value;
      }
      if (!cache.has(value)) {
        cache.set(value, value.bind(target));
      }
      return cache.get(value);
    }
  };
  const proxy = new Proxy(target, handler);
  return proxy;
}

const logger1 = selfish(new Logger());
const {printName} = logger1
printName(); // hi lee

19.10 name属性

class Person{};
Person.name // "Person"

19.11 Class的取值函数(getter)和存值函数(setter)

class MyClass{
    constructor(){}
    get prop(){
        return 'getter' // 如果return this.prop会导致死循环
    }
    set prop(value){
        console.log(`设置prop属性值为${value}`)
    }
}
let inst = new MyClass();
inst.prop = 'lee'; // 设置prop属性值为lee
inst.prop; // "getter"

存值函数和取值函数是设置在属性的desctriptor对象上的

class MyClass{
    constructor(state= {}){
        this.state = state;
    }
    get prop(){
        return this.state.prop;
    }
    set prop(value){
        this.state.prop = value;
    }
}
let inst = new MyClass();
var descriptor = Object.getOwnPropertyDescriptor(MyClass.prototype,"prop")
console.log("get" in descriptor)
console.log("set" in descriptor)

19.12 Class的Generator方法

如果某个方法之前加上星号* ,就表示该方法是一个Generator函数。

class MapArr{
    constructor(...args){
        this.args = args;
    }
    *[Symbol.iterator](){// 每次调用iterator.next就会调用一次该函数,如果是Generator函数更好一一对应
        for(let arg of this.args){
            yield arg;
        }
    }
}
var arr = new MapArr(1,2,3,4,5);
console.log([...arr]) // [1, 2, 3, 4, 5]

19.13 Class的静态方法

static关键字表示该方法不会被实例继承而是直接通过类调用

class MyClass{
    static classMethod(){
        console.log('hello')
    }
}
MyClass.classMethod() // hello(类调用)
var inst = new MyClass(); 
inst.classMethod(); //报错 Uncaught TypeError: inst.classMethod is not a function(实例上不可访问)

但是父类的静态方法可以被子类继承,同样需要类调用。

class Child extends MyClass{

}
Child.classMethod() // hello

子类也可以在super对象上调用

class Child2 extends MyClass{
    static classMehthod(){
        return super.classMethod()
    }
}
Child2.classMethod(); // hello

19.14 Class的静态属性和实例属性

class Foo{}
Foo.prop = 1;
Foo.prop; // 1(Class本身属性)

Class内部只有静态方法没有静态属性

19.14.1 Class的实例属性

Class的实例属性可以用等式写入类的定义之中。

class MyClass{
    myProp = 42;
    constructor(){
        console.log(this.myProp)
    }
}
var myClass = new MyClass(); // 输出 42

19.14.2 Class的静态属性

class MyClass{
    static myProp = 42;
}
console.log(MyClass.myProp) // 42(静态属性)

旧写法: MyClass.prop = 1;

19.15 new.target属性

构造函数不是使用new命令调用的,那么new.target会返回undefined,是使用new命令调用的话会返回函数名

function Person(name){
    if(new.target !== undefined){
        this.name = name;
    }else{
        throw new Error('必须使用new生成实例')
    }
}
var person1 = new Person('lee');
var person2 = Person.call(person1,'lee') // 报错 Uncaught Error: 必须使用new生成实例

另一种写法

function Person(name){
    if(new.target === Person){
        this.name = name;
    }else{
        throw new Error('必须使用new生成实例')
    }
}
var person1 = new Person('lee');
var person2 = Person.call(person1,'lee') // 报错 Uncaught Error: 必须使用new生成实例

第20章 Class的继承

20.1 简介

Class可以通过extends关键字实现继承

class Person{}
class HeightPerson extends Person{}
class Person{
    constructor(name,age){
        this.name = name; // 名字
        this.age = age; // 年龄
    }
    toString(){
        return `他叫${this.name},年龄${this.age}`
    }
}
class Basketballplayer extends Person{
    constructor(name,age,height){
        super(name,age) // 调用父类的constructor(name,age)人的名字和年龄等信息
        this.height = height; // 篮球运动员的身高
    }
    toString(){
        return super.toString() + `,身高为${this.height}的篮球运动员`
    }
}
var Lee = new Basketballplayer('lee','18',195)
Lee.toString(); // "他叫lee,年龄18,身高为195的篮球运动员"

子类必须在constructor方法中调用super,super表示父类的构造函数,用来创建新的this对象,否则会报错。 ES5的继承实质是先创造子类的实例对象this,然后在将父类的方法添加到this上面(Parent.apply(this))。ES6实质是先创造父类的实例对象(所以必须先调用super方法),然后再用子类的构造函数修改this。 如果子类没有定义constructor方法,那么这个方法会被默认添加。

class Basketballplayer extends Person{

}

等同于

class Basketballplayer extends Person{
    constructor(...args){
        super(...args)
    }
}

只有调用了super之后才能使用this关键字

class Person{
    constructor(name,age){
        this.name = name; // 名字
        this.age = age; // 年龄
    }
}
class Basketballplayer extends Person{
    constructor(name,age,height){
        this.height = height; // 篮球运动员的身高
        super(name,age) // 调用父类的constructor(name,age)人的名字和年龄等信息
    }
}
var Lee = new Basketballplayer('lee','18',195) // 报错 Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

实例对象lee同时是Person和Basketballplayer两个类的实例

class Person{
    constructor(name,age){
        this.name = name; // 名字
        this.age = age; // 年龄
    }
}
class Basketballplayer extends Person{
    constructor(name,age,height){
        super(name,age) // 调用父类的constructor(name,age)人的名字和年龄等信息
        this.height = height; // 篮球运动员的身高
    }
}
var lee = new Basketballplayer('lee',18,195)
console.log(lee instanceof Person)
console.log(lee instanceof Basketballplayer)

20.2 Object.getPrototypeOf()

接上例

Object.getPrototypeOf(Basketballplayer) //从子类获取父类
// class Person{...}

20.3 super关键字

super这个关键字既可以当作函数使用,也可以当作对象使用。 情况一: 作为函数调用时代表父类的构造函数,子类必须执行一次super函数。

class A{};
class B extends A{
    constructor(){
        super(); // 必须调用
    }
}

super代表父类A的构造函数,但是内部的this指向的是B。相当于A.prototype.constructor.call(this)

class A{
    constructor(){
        console.log(new.target.name)
    }
};
class B extends A{
    constructor(){
        super();
    }
}
new A(); // A
new B(); // B

super函数只能在子类构造函数中使用,其他地方会报错。

class A{};
class B extends A{
    m(){
        super();
    }
}
new B(); // Uncaught SyntaxError: 'super' keyword unexpected here

情况二:作为对象时在普通方法中指向父类的原型对象,静态方法中指向父类。

class A{
    p(){
        return 2;
    }
};
class B extends A{
    constructor(){
        super();
        console.log(super.p())
    }
}
new B(); // 2

相当于调用了A.prototype.p() super指向了父类的原型对象,父类实例上的属性和方法无法通过super调用,但是定义在原型上的属性和方法可以。

class A{
    constructor(){
        this.p = 2;
    }
};
class B extends A{
    get m(){
        return super.p;
    }
}
let b = new B();
b.m // undefined

定义在原型上就可以获取了

class A{};
A.prototype.p = 2;
class B extends A{
    get m(){
        return super.p;
    }
}
let b = new B();
b.m // 2

ES6规定,super方法调用父类方法时,super会绑定子类的this

class A{
    constructor(){
        this.x = 1;
    }
    print(){
        console.log(this.x)
    }
};
class B extends A{
    constructor(){
        super();
        this.x = 2;
        console.log(super.print())
    }
}
let b = new B(); // 2(绑定子类的this)

相当于调用super.print.call(this) constructor中super指向子类本身this,普通方法中super指向父类原型,即拥有自身的属性和共用方法。

20.4 类的prototype属性和_proto_属性

子类的_proto_属性代表构造函数继承,子类的prototype的_proto_属性代表方法继承。

class A{};
class B extends A{}
console.log(B.__proto__ === A);
console.log(B.prototype.__proto__ === A.prototype);

回忆原型链person.__proto__ === Person.prototype

20.4.1 extends的继承目标

20.4.2 实例的_proto_属性

子类原型的原型是父类的原型

class A{};
class B extends A{}
var a = new A();
var b = new B();
b.__proto__.__proto__ == a.__proto__; // true

第21章 修饰器

第22章 Module的语法

22.1 概述

CommonJS和AMD都是只能运行时确定模块依赖关系。CommonJS模块就是对象,输入时必须查找对象属性。

// CommonJS模块
let { a, b, c} = require('fs');

相当于

// CommonJS模块
let _fs = require('fs'); // 会把整个模块都加载进来
let { a,b,c} = _fs

ES6模块的设计思想是尽量静态化,使得编译的时候就能确定模块的依赖关系,以及输入输出的变量。

// ES6模块
import {a,b,c} from 'fs' // 只加载a,b,c这三个方法

22.2 严格模式

ES6模块自动采用严格模式。

22.3 export命令

模块功能主要由两个命令构成:export和import。即导出和导入。 export的写法一

export var a = 'a';
export var b = 'b';
export var c = 'c'; // 一个个导出时

写法二

var a = 'a';
var b = 'b';
var c = 'c';
export {a,b,c} // 要用花括号括起来,因为不止导出一个

输出类和函数

export function add(a,b){return a+b}

使用关键字as改变名称

function add(a,b){return a+b}
export {add as add1} 
export 1 // 报错 1 不是接口

export 语句输出的接口与其对应的值是动态绑定关系,即通过该接口可以取到模块内部的实时值。

export var foo = 1;
setTimeout(()=>foo = 2,500)

先是输出1,再变成2 export语句放在函数中报错

function add(){
    export default 'a'
} // 报错

22.4 import命令

使用import命令加载模块。

import {a,b} from './add.js'
function add(){
    return a + b
}

取别名

import {a as a1} from './add.js'

import命令具有提升效果,会提升到整个模块的头部并首先执行

func();
import {func} from './add.js'

静态执行,不能使用变量和表达式

import {a+'1'} from './index.js' // 报错
let module = 'index.js'
import {a+'1'} from module // 报错
if(x==1){
    import {a+'1'} from index.js' // 报错
}else{
    import {a+'1'} from index1.js' // 报错
}

import语句会执行所加载模块

import 'lodash'  // 执行

import 语句是单例模式

import {foo} from './index.js'
import {bar} from './index.js'

等同于

import {foo, bar} from './index.js'

22.5 模块的整体加载

整体加载

import * as allFunc from './index.js'

不允许下面操作

circle.foo =1;
circle.area = function(){}

22.6 export default 命令

export default指定默认输出

export default function(){}

其他文件加载时

import func from './index.js'

导出非匿名函数

export default function foo(){}

或者

function foo(){}
export default foo;

导入时取别名

import {default as default1} from './index.js'

下面操作错误

export default var a = 1; // 错误

下面操作正确

export default 42; // 正确

同时有export和export default时,导入写法如下

import func,{a,b,c} from './index.js' //funct为默认输出,其他为普通导出 

其实本质是把default后面的值赋给default

22.7 export 与 import的复合写法

导入后又导出

export {foo,bar} from './index.js'

使用别名

export {foo as foo1,bar} from './index.js'

整体导入又整体输出

export * from './index.js'

默认接口导入又导出

export {default} from './index.js'

具名接口改为默认接口

export {foo as default} from './index.js'

默认接口改为具名接口

export {default as foo} from './index.js'

22.8 模块的继承

export * from './index.js'
export default function(){
    return 1
}

export * 命令会忽略index.js模块的default 方法 改变属性或者方法名后输出

export {a as a1} from './index.js

加载上面模块

import * as all from './main.js;
import def from './main.js'

22.9 跨模块常量

多个常量的导出导入

export const a = 1;
export const b = 2;
export const c = 3;

导入

import * as all from './index.js'
console.log(all.a) // 1
console.log(all.b) // 2

另一种导入方式

import {a,b} from './index.js'
console.log(a) // 1
console.log(b) // 2

将多个文件集合后再输出

export const a = 1;
export const b = 2;

集合导出两个文件

export {a} from './index.js'
export {b} from './index1.js'

导入

import {a,b} from './main.js'

22.10 import()

按需加载

import('./index.js').then(()=>{})

条件加载

if(condition){
    import('./index').then(...)
}

动态的模块路径

import(f()).then(...)

第23章 Module的加载实现

23.1 浏览器加载

23.1.1 传统方法

html页面中,浏览器通过<script>标签来加载脚本 内嵌脚本

<script type="application/javascript">
    // 脚本代码
</script>

外部脚本

<script type="application/javascript" src="./index.js"></script>

type属性可以忽略 浏览器同步加载js脚本,渲染引擎遇到script标签就会停下来,必须等待脚本执行完毕,外部脚本还需加入脚本下载时间。 防止下载时间过长导致的浏览器卡死,一般使用浏览器脚本的异步加载。 defer: 等到浏览器渲染结束才执行

<script type="application/javascript" src="./index.js" defer></script>

async: 一旦下载完成就终断渲染,来执行。多个async不能保证其执行顺序。

<script type="application/javascript" src="./index.js" async></script>

23.1.2 加载规则

浏览器ES6模块使用script标签,使用type属性,具体为type="module"

<script type="module" src="./index.js"></script>

异步加载相当于

<script type="module" src="./index.js" defer></script>

使用async属性也是完全可以的,会中断浏览器渲染,执行完毕后再渲染

<script type="module" src="./index.js" async></script>

也允许内嵌在网页内部,相当于加载外部脚本

<script type="module">
    // 你的代码
</script>

23.2 ES6模块和CommonJS模块的差异

差异: 1、CommonJS模块是对值的复制,ES6模块输出是对值的引用。 2、CommonJS模块是运行时加载,ES6模块是编译时输出接口。 ES6模块对值的更改会动态反映到所有的引入文件,引入的参数只读不可赋值,除非是对象则可以更改属性。不同脚本加载同一个模块得到的都是同一个实例。 CommonJS模块

// lib.js
var a = 1
module.exports = {
    a: a
}
// main.js
var mod = require('./lib.js')

23.3 Node加载

23.3.1 概述

Node加载中,一个模块脚本只要一行import或export语句,node就会认为这个脚本为ES6模块,否则为commonjs模块。如果不输出任何模块但是想要被node认为是ES6模块,可以加一句 export {};

23.3.2 import 命令加载CommonJS模块

一个CommonJS模块

// a.js
module.exports = {
    foo: 1
}

等同于

export default {
    foo: 1
}

使用import命令加载

import all from './a'
// all = {foo: 1}

或者

import {default as all} from './a'
// all = {foo: 1}

CommonJS模块的输出缓存机制在ES6加载方式下依然有效

module.exports = 123 // 缓存机制导致导出将会一直是123,不会变为null
setTimeout(_=>module.exports = null)

使用星号导入的例子

module.exports = null 
import * as all from './b'
// all = {default : null}(全导入时会自动添加default属性)

23.3.3 require 命令加载ES6模块

采用require加载ES6模块

// es.js
let foo = {bar : 1}
export default foo;
foo = null
// require.js
const es_namespace = require('./es')
console.log(es_namespace.default) // {bar: 1}(存在缓存机制,不会变为null

23.4 循环加载

容易出现互相依赖的情况

23.4.1 CommonJS模块的加载原理

node内部加载模块后会生成一个对象,需要用到时就会到exports属性上取,即使再次require加载也不会再次执行模块,而是到缓存中读取。 略

23.4.2 CommonJS模块的加载

引入时会停下来执行另一个脚本,另一个文件再引入前一个脚本时,即前一个脚本未执行完毕时只返回执行部分。 略

23.4.3 ES6模块的循环加载

如果a.js和b.js互相加载,执行a.js 由于 a.js的第一行是加载 b. js,所以先执行的是 b.js。而 b.js的第一行又是加载 a.js ,这时由于 a.js已经开始执行,所以不会重复执行,而是继续执行 b.js,b.js中输出a.js引入的值为undefined,因为a.js没执行完毕。简而言之,就是不会重复执行。

23.5 ES6模块的转码