深入理解ES6

1,954 阅读45分钟

本文系《深入理解ES6》读书笔记

第一章 块级作用域绑定

var 声明初始化变量, 声明可以提升,但初始化不可以提升。

一、块级声明:

  1. let
  2. const
    • 默认使用,在某种程度上实现代码不可变,减少错误发生的几率
    • 如果常量是对象,则对象中的值可以修改
  3. 不能重复声明,声明不会提升

二、临时死区:

JS引擎在扫描代码发现变量声明时,要么将它们提升至作用域顶部(var声明),要么将声明放到TDZ(临时死区)中(letconst声明)。访问TDZ中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会从TDZ中移出,然后可以正常访问。

第一种情况:

if(condition) {
    console.log(typeof value); //引用错误!
    let value = "blue"; //TDZ
}

第二种情况:

console.log(typeof value); //'undefined'-->这里不报错,只有变量在TDZ中才会报错
if(condition) {
    let value = "blue";
}

三、循坏中块作用域绑定

  • 立即调用(IIFE)

  • letconst之所以可以在运用在for-infor-of循环中,是因为每次迭代会创建一个新的绑定(const在for循环中会报错)。

四、全局块作用域绑定

var 可能会在无意中覆盖一个已有的全局属性, letconst会在全局作用域下创建一个新的绑定,但该绑定不会添加为全局对象的属性。换句话说,使用letconst不能覆盖全局变量,而只能遮蔽它。如果不是为全局对象创建属性,使用letconst要安全得多。

注:如果希望在全局对象下定义变量,仍然可以使用var。这种情况常见于在浏览器中跨frame或跨window访问代码

第二章 字符串和正则表达式

一、UTF-8码位

名词解释:

  • 码位: 每一个字符的“全球唯一的标识符,从0开始的数值”
  • 字符编码:表示某个字符的数值或码位即为该字符的字符编码。
  • 基本多文种平面(BMP,Basic Multilingual Plane)

    在UTF-16中,前2^16个码位均以16位的编码单元表示,这个范围被称作基本多文种平面

二、codePointAt()、 fromCodePoint() 和 normalize()

  • 两个方法对应于charCodeAt()fromCharCode()
  • normalize(): 规范的统一,适用于比较排序,国际化。

三、正则表达式 u 和 y 修饰符、正则表达式的复制、flag属性

  • u: 编码单元 ---> 字符模式

这个方法尽管有效,但是当统计长字符串中的码位数量时,运动效率很低。因此,你也可以使用字符串迭代器解决效率低的问题,总体而言,只要有可能就尝试着减小码位计算的开销。

检测u修饰符支持:

function hasRegExpU() {
    try {
        var pattern = new RegExp('.', 'u');
        return true;
    } catch (ex) {
        return false;
    }
}
  • y: 第一次匹配不到就终止匹配

当执行操作时, y修饰符会把上次匹配后面一个字符的索引保存到lastIndexOf中;如果该操作匹配的结果为空,则lastIndexOf会被重置为0。g修饰符的行为类似。

1. 只有调用exec()和test()这些正则表达式对象的方法时才会涉及lastIndex属性;
2. 调用字符串的方法,例如match(),则不会触发粘滞行为。
  • 正则表达式的复制
var re1 = /ab/i;
re2 = new RegExp(re1); //没有修饰符复制
re3 = new RegExp(re1, "g"); //有修饰符(ES6)
  • flag属性 --- 获取正则表达式的修饰符

es5方法获取正则表达式的修饰符:

function getFlags(re) {
    var text = re.toString();
    return text.substring(text.lastIndexOf('/' + 1, text.length);
}

模板字面量

多行字符串

基本的字符串格式化(字符串占位符)

HTML转义

  • 标签模板
function passthru(literals, ...substitutions) {
    //返回一个字符串
    let result = "";
    //根据substitutions的数量来确定循环的执行次数
    for(let i=0; i<substitutions.length; i++){
        result += literals;
        result += substitutions[i]
        console.log(literals[i]) 
        console.log(substitutions[i])
    }
    
    //合并最后一个literal
    result += literals[literals.length - 1];
    return result;
}

let count = 10;
price = 0.25;
message = passthru`${count} items cost $${(count * price).toFixed(2)}`;

console.log(message)
  • String.raw
String.raw`assda\\naadasd`

//代码模拟(略)

第三章 函数

一、默认参数值

ES5默认参数值

下面函数存在什么问题 ???

function makeRequest(url, timeout, callback) {
    
    timeout = timeout || 2000;
    callback = callback || function() {};
    
}

假如timeout传入值0,这个值是合法的,但是也会被视为一个假值,并最终将timeout赋值为2000。在这种情况下,更安全的选择是通过typeof检查参数类型,如下:

function makeRequest(url, timeout, callback) {
    
    timeout = (typeof timeout !== 'undefined') ? timeout :2000;
    callback = (typeof callback !== 'undefined') ? callback : function() {};
    
}

ES5默认参数值

function makeRequest(url, timeout = 2000, callback) {
    
    //函数的其余部分
    
}

//特别注意:此时 null 是一个合法值,所以不会使用 timeout 默认值,即 timeout = null
makeRequest('/foo', null, function(body){
    doSomething(body);
})

二、默认参数值对arguments的影响**

  • ES5:

非严格模式:参数变化,arguments对象随之改变;

严格模式:无论参数如何变化,arguments对象不再随之改变;

  • ES6

非严格模式/严格模式:无论参数如何变化,arguments对象不再随之改变;

注: 在引用参数默认值的时候,只允许引用前面参数的值,即先定义的参数不能访问后定义的参数。这可以用默认参数的临时死区来解释。如下:

function add(first = second, second) {
    return first + second;
}

console.log(add(1, 1)); //2
console.log(add(undefined, 1)) //抛出错误

//解释原理:
//add(1, 1)
let first = 1;
let second = 1;
//add(undefined, 1)
let first = second;
let second = 1; //处于临时死区

三、不定参数的使用限制

  1. 每个函数最多只能声明一个不定参数,而且一定要放在所有参数的末尾。
  2. 不定参数不能用于对象字面量setter之中(因为对象字面量setter的参数有且只有一个,而在不定参数的定义中,参数的数量可以无限多

无论是否使用不定参数,arguments对象总是包含所有传入函数的参数。

四、展开运算符

let value = [25, 50, 75, 100];
//es5
console.log(Math.max.apply(Math, values); //100
//es6
console.log(Math.max(...values)); //100

五、name 属性

两个有关函数名称的特例:

  1. 通过bind()函数创建的函数,其名称将带有“bound”前缀;
  2. 通过Function构造函数创建的函数,其名称将是“anonymous”.
var doSomething = function() {
    //空函数
}

console.log(doSomething.bind().name); //'bound doSomething'
console.log((new Function()).name); //'anonymous(匿名)'

切记: 函数name属性的值不一定引用同名变量,它只是协助调试用的额外信息,所以不能使用name属性的值来获取对于函数的引用。

六、明确函数的多重用途

JS函数有两个不同的内部方法:[[call]][[Construct]]

  • 当通过new关键字调用函数是,执行的是 [[Construct]] 函数,它负责创建一个通常被称为实例的新对象,然后再执行函数体,将this绑定到实例上(具有 [[Construct]] 方法的函数被统称为构造函数,箭头函数没有 [[Construct]] 方法 );
  • 如果不通过 new 关键字调用函数,则执行 [[call]] 函数,从而直接执行代码中的函数体;

七、元属性(Metaproperty)new.target

为了解决判断函数是否通过new关键字调用的问题,new.target横空出世 (instance of ---> new.target)

在函数外使用new.target是一个语法错误。

八、块级函数

  • ES5严格模式下,代码块中声明函数会报错;
  • ES6严格模式下, 可以在定义该函数的代码块中访问和调用它 (块级函数提升,let变量不提升);
  • ES6非严格模式下,函数不再提升至代码块的顶部,而是提升至外围函数或全局作用域的顶部。

九、箭头函数

箭头函数与传统的JS函数不同之处主要有以下几个方面:

  1. 没有thissuperargumentsnew.target绑定;
  2. 不能通过new关键字调用;
  3. 没有原型;
  4. 不可以改变this的绑定;
  5. 不支持arguments对象
  6. 不支持重复的命名参数

创建一个空函数

let doNothing = () => {};

返回一个对象字面量

let getTempItem = id => ({ id: id, name: "Temp"});

创建立即执行的函数

let person = ((name) => {
    
    return {
        getName: function() {
            return name;
        }
    }
    
})("xszi")

console.log(person.getName()); //xszi

箭头函数没有this绑定

let PageHandler = {
    
    id: '123456',
    init: function() {
        document.addEventListener("click", function(event){
            this.doSomething(event.type); //抛出错误
        }, false)
    },
    
    doSomething: function(type) {
        console.log("handling " + type + "for" + this.id)
    }
    
}

使用bind()方法将函数的this绑定到PageHandler,修正报错:

let PageHandler = {
    
    id: '123456',
    init: function() {
        document.addEventListener("click", (function(event){
            this.doSomething(event.type); //不报错
        }).bind(this), false)
    },
    
    doSomething: function(type) {
        console.log("handling " + type + "for" + this.id)
    }
    
}

使用箭头函数修正:

let PageHandler = {
    
    id: '123456',
    init: function() {
        document.addEventListener("click", 
            event => this.doSomething(event.type), false);
    },
    
    doSomething: function(type) {
        console.log("handling " + type + "for" + this.id)
    }
    
}
  • 箭头函数没有 prototype属性,它的设计初衷是 即用即弃, 不能用来定义新的类型。
  • 箭头函数的中this取决于该函数外部非箭头函数的this值,不能通过call(), apply()bind()方法来改变this的值。

箭头函数没有arguments绑定

始终访问外围函数的arguments对象

十、尾调用优化

  • ES5中,循环调用情况下,每一个未完成的栈帧都会保存在内存中,当调用栈变的过大时会造成程序问题。
  • ES6中尾调用优化,需要满足以下三个条件:
    • 尾调用不访问当前栈帧的变量(也就是说函数不是一个闭包);
    • 在函数内部,尾调用是最后一条语句;
    • 尾调用的结果作为函数值返回;

如何利用尾调用优化

function factorial(n) {
    if ( n<=1 ) {
        return 1;
    }
}else{
    
    //引擎无法自动优化,必须在返回后执行乘法操作 
    return n * factorial(n-1);
    //随调用栈尺寸的增大,存在栈溢出的风险
}
function factorial(n, p = 1) {
    if ( n<=1 ) {
        return 1 * p;
    }
}else{
    let result = n * p;
    //引擎可自动优化
    return  factorial(n-1, result);
    //不创建新的栈帧,而是消除并重用当前栈帧
}

第四章 扩展对象的功能性

一、对象的类别

  • 普通对象
  • 特异对象
  • 标准对象(ES6中规范中定义的对象,如Array,Date)
  • 内建对象:

脚本开始执行时存在于JS执行环境中的对象,所有标准对象都是内建对象。

var person = {
    name: 'xszi',
    sayName: function() {
        console.log(this.name);
    }
}

var person = {
    name: 'xszi',
    sayName() {
        console.log(this.name);
    }
}

//两者唯一的区别是,简写方式可以使用super关键字

二、可计算属性名(Computed Property Name)

在对象字面量中使用方括号表示的该属性名称是可计算的,它的内容将被求值并被最终转化为一个字符串。

如下:

var suffix = ' name';

var person = {
    ['first' + suffix]: 'xszi',
    ['last' + suffix]: 'wang'
};

console.log(person['first name']); //xszi
console.log(person['last name']) // wang

三、新增方法

ECMAScript 其中一个设计目标是:不再创建新的全局函数,也不在Object.prototype上创建新的方法。

  • Object.is()

大多数情况下,Object.is()与'==='运行结果相同,唯一区别在于 +0-0 识别为不相等,并且NaNNaN等价。

  • Object.assign()

mixin()方法使用赋值操作符(assignment operator)= 来复制相关属性,却不能复制 访问器属性 到接受对象中,因此最终添加的方法弃用mixin而改用assign作为方法名。

Object.assign() 方法可以接受任意数量的源对象,并按指定的的顺序将属性赋值到接收对象中。所以如果多个源对象具有同名属性,则排位靠后的源对象会覆盖排位靠前的。

访问器属性Object.assign() 方法不能将提供者的访问器属性赋值到接收对象中。由于 Object.assign()方法执行了赋值操作,因此提供者的访问器属性最终会转变为接受对象中的一个数据属性。

eg:

 var receiver = {};
     supplier = {
         get name() {
             return 'file.js'
         }
     };
     
Object.assign(receiver, supplier);

var descriptor = Object.getOwnPropertyDescriptor(receiver, 'name');

console.log(descriptor.value); // 'file.js'
console.log(descriptor.get); // undefined

四、自有属性枚举顺序

自有属性枚举顺序的基本规则是:

  1. 所有数字键按升序排序;
  2. 所有字符串键按照它们被加入对象的顺序排序;
  3. 所有symbol按照他们被加入的顺序排序。

五、增强对象的原型

  • 改变对象的原型
Object.setPrototypeOf(targetObject, protoObject);
  • 简化原型访问的Super引用

Super 引用相当于指向对象原型的指针,实际上也就是Object.getPrototypeOf(this), **必须要在使用简写方法的对象中使用 Super**引用。

Super引用不是动态变化的,它总是指向正确的对象。

六、正式的方法定义

ES6正式将方法定义为一个函数,它会有一个内部的 [[HomeObject]] 属性来容纳这个方法从属的对象。

Super的所以引用都通过 [[HomeObject]] 属性来确定后续的运行过程:

  1. [[HomeObject]] 属性上调用Object.getPrototypeOf()方法来检索原型的引用;
  2. 搜索原型找到同名函数;
  3. 设置this绑定并且调用相应的方法。

第五章 解构: 使数据访问更便捷

一、对象解构


let node = {
    type: "Indetifier",
    name: "foo"
}

let {type, name} = node;

console.log(type); // Indetifier
console.log(name); // foo

不要忘记初始化程序(也就是符号右边的值)

var {type, name}; //报错,使用let和const同样报错
// 除使用解构外,使用var, let不强制要求提供初始化程序, 但是const一定需要;

二、解构赋值

let node = {
    type: "Indetifier",
    name: "foo"
}

type = 'Literal', name = 5;

//使用解构语法为多个变量赋值
({type, name} = node);  //需要使用()包裹解构复制语句,{}是一个代码块,不能放在左边

console.log(type); // Indetifier
console.log(name); // foo
  • 默认值与上章的 默认参数 类似
  • 为非同名布局变量赋值
let node = {
   type: "Indetifier",
   name: "foo"
}

let { type: localType, name: localName } = node;

console.log(localType); // Indetifier
console.log(localName); // foo

type: localType语法的含义是读取名为type的属性并将其只存储在变量localType中。

  • 嵌套对象解构

三、数组解构

  • 解构赋值

数组解构也可用于赋值上下文,但不需要用小括号包裹表达式,这一点与对象解构的的约定不同。

let colors = ['red', 'green', 'blue'], firstColor = 'black', secondColor = 'purple';

[firstColor, secondColor] = colors;

console.log(firstColor); // 'red'
console.log(secondColor); // 'green'

交换值


let a = 1, b = 2;

[a, b] = [b, a];

console.log(a); //2
console.log(b); //1

  • 嵌套数组解构(地址的解构赋值)
let colors = ['red', ['green', 'lightgreen'], 'blue'];

let [firstColor, [secondColor]] = colors;

console.log(firstColor); //red
console.log(secondColor); //green

  • 不定元素(在被解构的数组中,不定元素必须为最后一个条目,在后面继续添加逗号会导致程序抛出语法错误)
  • 混合解构(混合对象和数组解构,使得我们从JSON配置中提取信息时,不再需要遍历整个结构了。)
  • 解构参数
function setCookie(name, value, options) {

    options = options || {};
    
    let secure = options.secure,
        path = options.path,
        domian= options.domain,
        expires = options.expires;
        
    //设置cookie代码
}

// 第三个参数映射到options中

setCookie('type', 'js', {
    secure: true,
    expires: 60000
})

上面函数存在一个问题:仅查看函数的声明部分,无法辨识函数的预期参数,必须通过阅读函数体才可以确定所有参数的情况。可以使用 解构参数 来优化:

function setCookie(name, value, {secure, path, domain, expires}}) {
    //设置cookie代码
}

setCookie('type', 'js',{
    secure: true,
    expires: 60000
})
  1. 必须传值的解构参数;
  2. 解构参数的默认值。

第六章 SymbolSymbol属性

Symbol出现之前,人们一直通过属性名来访问所有属性,无论属性名由什么元素构成,全部通过一个字符串类型的名称来访问;私有名称原来是为了让开发者们创建非字符串名称而设计的,但是一般的技术无法检测这些属性的私有名称。

通过Symbol可以为属性添加非字符串名称,但是其隐私性就被打破了。

一、创建、使用SymbolSymbol共享体系

  • 创建、使用
let firstName = Symbol();
let person = {};

person[firstName] = 'xszi';
console.log(person[firstName]); //xszi

由于Symbol是原始值,因此调用new Symbol()会导致程序抛出错误。

Symbol函数接受一个可选参数,其可以让你添加一段文本描述即将创建的Symbol,这段描述 不可用于属性访问。该描述被存储在内部的 [[Description]] 属性中,只有当调用SymboltoString()方法时才可以读取这个属性。

  • Symbol共享体系

有时我们可能希望在不同代码中共享同一个Symbol(在很大的代码库中或跨文件追踪Symbol非常困难),ES6提供了一个可以全局访问的全局Symbol注册表,即使用Symbol.for()方法。

let uid = Symbol.for('uid');
let object = {};

object[uid] = '12345';

console.log(object[uid]); //'12345'
console.log(uid); // 'Symbol(uid)'

实现原理Symbol.for()方法首先在全局Symbol注册表中搜索键为‘uid’的Symbol是否存在,如果存在,直接返回已有的Symbol;否则,创建一个新的Symbol,并使用这个键在Symbol全局注册表中注册,随即返回新创建的Symbol

可以使用Symbol.keyFor()方法在Symbol全局注册表中检索与Symbol有关的键。

Symbol全局注册表是一个类似全局作用域的共享环境,也就是说你不能假设目前环境中存在哪些键。当使用第三方组件时,尽量使用Symbol键的命名空间减少命名冲突。如 jQuery.

二、Symbol与类型强制转换,属性检索

  • console.log()会调用SymbolString()方法
desc = String(uid);

desc = uid + ''; //报错,不能转为字符串类型

desc = uid / 2; //报错,不能转为数字类型
  • 属性检索
    • Object.keys() 返回可枚举属性
    • Object.getOwnPropertyNames() 不考虑可枚举性,一律返回
    • Object.getOwnProperty-Symbols() ES6用来检索对象中的Symbol属性

所有对象一开始没有自己独有的属性,但是对象可以从原型链中继承Symbol属性。

三、通过well-know Symbol暴露内部操作

还是不怎么理解,找到一个使用Symbol的实际场景才能更好理解!

第七章 Set集合与Map集合

Set 和 Map 主要的应用场景在于 数据重组数据储存

Set 是一种叫做集合的数据结构,Map 是一种叫做字典的数据结构

一、用对象属性模拟Set和Map集合

//set
var set = Object.create(null);

set.foo = true;

//检查属性是否存在
if(set.foo){
    //要执行的代码
}
//map
var map = Object.create(null);

map.foo = "bar";

//获取已存值
var value = map.foo;

console.log(value);

一般来说,Set集合常被用于检查对象中是否存在某个键名,而Map集合常被用来获取已存的信息。

所有对象的属性名必须是字符串类型,必须确保每个键名都是字符串类型且在对象中是唯一的

二、Set集合

- 有序
- 不重复

+0和-0在Set中被认为是相等的。

Set构造函数可以接受所有可迭代对象作为参数
  • Set中的方法: add、has、delete、clear,forEach,size(属性)

forEach遍历Set,回调函数中value和key的值相等,我的理解: Set集合中的元素都是不重复的,所以可以把值作为键,即“以值为键”。如下:

let set = new Set([1, 2]);

set.forEach(function(value, key, ownerSet)){
    console.log(key + " " + value);
    console.log(ownerSet === set);
});

在回调函数中使用this引用

let set = new Set([1, 2]);

let processor = {
    output(value) {
        console.log(value);
    },
    process(dataSet) {
        dataSet.forEach(function(value){
            this.output(value);
        }, this);
    }
};

processor.process(set);

箭头函数this

let set = new Set([1, 2]);

let processor = {
    output(value) {
        console.log(value);
    },
    process(dataSet) {
        dataSet.forEach(value => this.output(value));
    }
};

processor.process(set);
  • 将Set集合转换为数组
let set = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
    array = [...set];
    
console.log(array); //[1, 2, 3, 4, 5]
function eliminbateDuplicates(items){
    return [...new Set(items)]
}

let numbers = [1, 2, 3, 3, 3, 4, 5];
    noDuplicates = eliminateDuplicates(numbers);

console.log(noDuolicates); //[1, 2, 3, 4, 5]

三、 Weak Set 集合

解决Set集合的强引用导致的内存泄漏问题

Weak Set集合只存储对象的弱引用,并且不可以存储原始值;集合中的弱引用如果是对象唯一的引用,则会被回收并释放相应内存。

Weak Set集合的方法:add, has,delete

  • SetWeak Set 的区别:
差异 Set Weak Set
最大区别 保存对象值的强引用 保存对象值的弱引用
方法传入非对象参数 正常 报错
可迭代性 可迭代 不可迭代
支持forEach方法 支持 不支持
支持size属性 支持 不支持

四、 Map集合

- 有序
- 键值对

在对象中,无法用对象作为对象属性的键名;但是Map集合中,却可以这样做。

let map = new Map(),
    key1 = {},
    key2 = {};
    
map.set(key1, 5);
map.set(key2, 42);

console.log(map.get(key1)); //5
console.log(map.get(key2)); //42

以上代码分别用对象 key1key2 作为两个键名在Map集合里存储了不同的值。这些键名不会强制转换成其他形式,所以这两个对象在集合中是独立存在的,也就是说,不需要修改对象本身就可以为其添加一些附加信息

Map集合的方法:setgethas(key)delete(key)clearforEachsize(属性)

Map集合初始化过程中,可以接受任意数据类型的键名,为了确保它们在被存储到Map集合中之前不会被强制转换为其他数据类型,因而只能将它们放在数组中,因为这是唯一一种可以准确地呈现键名类型的方式。

五、 Weak Map集合

无序 键值对

- 弱引用Map集合,集合中键名必须是一个对象,如果使用非对象键名会报错;
- 键名对于的值如果是一个对象,则保存的是对象的强引用,不会触发垃圾回收机制。
  • Weak Map 最大的用途是保存Web页面中的DOM元素。
let map = new WeakMap(),
    element = document.querySelector('.element');
    
map.set(element, "Original");

let value = map.get(element);
console.log(value); //"Original"

//移除element元素
element.parentNode.removeChild(element);
element = null;

//此时 Weak Map集合为空,数据被同步清除

  • Weak Map集合的方法

set, get, has, delete

私有对象数据

存储对象实例的私有数据是Weak Map的另外一个应用:

var Person = (function(){
    var privateData = {},
    privateData = 0;
    
    function Person(name){
        Object.defineProperty(this, "_id", { value: privateId++ });
        
        privateData[this._id] = {
            name: name
        };
    }
    
    Person.prototype.getName = function() {
        return privateData[this._id].name;
    }
    
    return Person;
}());

上面这种方法无法获知对象实例何时被销毁,不主动管理的话,privateData中的数据就永远不会消失,需要使用Weak Map来解决这个问题。

let Person = (function(){
    let privateData = new WeakMap(),
    privateData = 0;
    
    function Person(name){
        privateData.set(this, {name: name});
    }
    
    Person.prototype.getName = function() {
        return privateData.get(this).name;
    }
    
    return Person;
}());

当你要在Weak Map集合与普通的Map集合之间做出选择时,需要考虑的主要问题是,是否只用对象作为集合的键名。

第八章 迭代器(Iterator)和生成器(Generator)

迭代器的出现旨在消除循环复杂性并减少循环中的错误。

一、什么是迭代器?

迭代器是一种特殊对象,他具有一些专门为迭代过程设计的专有端口,所有的迭代器对象都有一个next()方法,每次调用都返回一个结果对象。

ES5语法 实现一个迭代器

function createIterator(items) {
    
    var i = 0;
    
    return {
        next: function() {
        
            var done = (i >= items.length);
            var value = !done ? items[i++] : undefined;
            
            return {
                done: done,
                value: value
            };
        }
    }
}

var iterator = createIterator([1, 2, 3]);

console.log(iterator.next()); //"{ value: 1, done: false}"
console.log(iterator.next()); //"{ value: 2, done: false}"
console.log(iterator.next()); //"{ value: 3, done: false}"
console.log(iterator.next()); //"{ value: undefined, done: true}"

二、什么是生成器?

生成器是一种返回迭代器的函数,通过function关键字后的星号(*)来表示,函数中会用到新的关键字yield

function *createIterator() {
    yield 1;
    yield 2;
    yield 3;
}

let iterator = createIterator();

console.log(iterator.next().value); //1
console.log(iterator.next().value); //2
console.log(iterator.next().value); //3

yield的使用限制

yield关键字只可在生成器内部使用,在其他地方使用会导致程序抛出语法错误,即使在生成器内部的函数里使用也会报错,与return关键字一样,不能穿透函数的边界。

不能用箭头函数来创建生成器

三、可迭代对象和for-of循环

可迭代对象具有Symbol.iterator属性,是一种与迭代器密切相关的对象。Symbol.iterator通过指定的函数可以返回一个作用于附属对象的迭代器。

  • 检测对象是否为可迭代对象
function isIterable(object) {
    return typeof object[Symbol.iterator] === 'function';
}

console.log(isIterable([1, 2, 3])); //true
  • 创建可迭代对象

默认情况下,开发者定义的对象都是不可迭代对象,但如果给Symbol.iterator添加一个生成器,则可以将其变为可迭代对象。

let collection = {
    items: [],
    *[Symbol.iterator]() {
        for (let item of this.items) {
            yield item;
        }
    }
}

collection.items.push(1);
collection.items.push(2);
collection.items.push(3);

for (let x of collection) {
    console.log(x);
}

1
2
3

四、内建迭代器

  • 集合对象迭代器

    • entries()
    • values()
    • keys()
  • 字符串迭代器

  • NodeList迭代器

五、高级迭代器功能

  • 给迭代器传递参数
function *createIterator() {
    let first = yield 1;
    let second = yield first + 2; //4 + 2
    yield second + 3; //5 + 3
}

let iterator = createIterator();

console.log(iterator.next()); // '{ value: 1, done: false }'
console.log(iterator.next(4)); // '{ value: 6, done: false }'
console.log(iterator.next(5)); // '{ value: 8, done: false }'
console.log(iterator.next()); // '{ value: undefined, done: true }'
  • 在迭代器中抛出错误

调用next()方法命令迭代器继续执行(可能提供一个值),调用throw()方法也会命令迭代器继续执行,但同时也抛出一个错误,在此之后的执行过程取决于生成器内部的代码。

  • 生成器返回语句

展开运算符与for-of循环语句会直接忽略通过return语句指定的任何返回值,只要done一变为true就立即停止读取其他的值。

  • 委托生成器

在生成器里面再委托另外两个生成器

  • 异步任务执行(******)

生成器令人兴奋的特性与异步编程有关。

function run(taskDef) {
    
    //创建一个无使用限制的迭代器
    let task = taskDef();
    
    //开始执行任务
    let result = task.next();
    
    //循环调用next() 的函数
    function step() {
    
        //如果任务未完成,则继续执行
        if(!result.done){
            result = task.next();
            //result = task.next(result.value) 向任务执行器传递数据
            step();
        }
    }
    
    //开始迭代执行
    step();
}

第九章 JavaScript 中的类

ES6中的类与其他语言中的还是不太一样,其语法的设计实际上借鉴了Javascript的动态性。

ES5 中的近类结构,创建一个自定义类型:

  1. 首先,创建一个构造函数;
  2. 然后,定义另一个方法并赋值给构造函数的原型。
function PersonType(name) {
    this.name = name;
}

PersonType.prototype.sayName = function() {
    console.log(this.name);
}

var person = new PersonType('waltz');
person.sayName(); //'waltz'

console.log(person instanceof PersonType); //true
console.log(person instance of Object); //true

一、 类的声明

  • 基本的类声明方法
class PersonClass {
    
    //等价于PersonType的构造函数
    constructor(name){
        this.name = name;
    }
    
    //等价于PersonType.protoType.sayName
    sayName() {
        console.log(this.name);
    }
}

let person = new PersonClass('waltz');
person.sayName(); //'waltz'

console.log(person instanceof PersonClass); //true
console.log(person instanceof Object); //true

console.log(typeof PersonClass); //'function'
console.log(typeof PersonClass.prototype.sayName) //'function'

自有属性是实例中的属性,不会出现在原型上,且只能在类的构造函数或方法中创建。建议你在构造函数中创建所有的自有属性,从而只通过一处就可以控制类中的所有自有属性。

与函数不同的是,类属性不可被赋予新值。

二、 为何使用类语法

  • 函数声明可以被提升,而类声明与let声明类似,不能被提升;真正执行声明语句之前,它们会一直存在于临时死区(TDZ)中。
  • 类声明中的所有代码将自动运行在严格模式下,而且无法强行让代码脱离严格模式执行。
  • 在自定义类型中,需要通过Object.defineProperty() 方法手工指定某个方法为不可枚举;而在类中,所有的方法都是不可枚举的。
  • 每个类都有一个名为[[Construct]]的内部方法,通过关键字new调用那些不含[[Construct]]的方法会导致程序抛出错误。
  • 使用除关键字new以外的方式调用类的构造函数会导致程序抛出错误。
  • 在类中修改类名会导致程序报错。

三、类表达式

和函数的声明形式和表达式类似。

在js引擎中,类表达式的实现与类声明稍有不同。对于类声明来说,通过let定义的外部绑定与通过const定义的内部绑定具有相同的名称。而命名类表达式通过const定义名称,从而只能在类的内部使用。

四、作为一等公民的类

在程序中。一等公民是指一个可以传入函数,可以从函数返回,并且可以赋值给变量的值。(JS函数是一等公民)

function createIbject(classDef) {
    return new classDef();
}

let Obj = createObject(class {

    sayHi() {
        console.log('Hi!')
    }
});
obj.sayHi(); //'Hi!'

类表达式还有另一种使用方式,通过立即调用类构造函数可以创建单例。用new调用类表达式,紧接着通过一对小括号调用这个表达式:

let person = new class {

    constructor(name) {
        this.name = name;
    }
    
    sayName() {
        console.log(this.name);
    }
}('waltz');

person.sayName(); // 'waltz'

依照这种模式可以使用类语法创建单例,并且不会再作用域中暴露类的引用。

五、访问器属性

class CustomHtmlElement() {

    constructor(element){
        this.element = element;
    }
    
    //创建getter
    get html() {
        return this.element.innerHTML;
    }
    
    //创建setter
    set html(value) {
        this.element.innnerHTML = value;
    }
    
}

var descriptor = Object.getOwnPropertyDescriptor(CustomHtmlElement.prototype, "html");
console.log("get" in descriptor); //true
console.log("set" in descriptor); //true
console.log(descriptor.enumerable); //false

六、可计算成员名称

//类方法
let methodName = "sayName";

class PersonClass(name) {
    
    constructor(name) {
        this.name = name;
    }
    
    [methodName]() {
        console.log(this.name);
    }
};

let me = new PersonClass("waltz");
me.sayName(); // 'waltz'
//访问器属性
let propertyName = 'html';
class CustomHTMLElement)() {
    
    constructor(element) {
        this.element = element;
    }
    
    get [propertyName]() {
        return this.element.innerHTML;
    }
    
    set [propertyName](value) {
        this.element.innerHTML = value;
    }
}

七、生成器方法


class MyClass {

    *createIterator() {
        yield 1;
        yield 2;
        yield 3;
    }
    
}

let instance = new MyClass();
let iterator = instance.createIterator();

如果用对象来表示集合,又希望通过简单的方法迭代集合中的值,那么生成器方法就派上用场了。

尽管生成器方法很实用,但如果你的类是用来表示值的 集合 的,那么为它定义一个 默认迭代器 更有用。

八、静态成员

直接将方法添加到构造函数中来模拟静态成员是一种常见的模式。

function PersonType(name) {
    this.name = name;
}

//静态方法
PersonType.create = function(name) {
    return new PersonType(name);
}

//实例方法
PersonType.protoType.sayName = function() {
    console.log(this.name);
};

var person = PersonType.create('waltz');

类等价:

class PersonClass {

    // 等价于PersonType构造函数
    constructor(name) {
        this.name = name;
    }
    
    //等价于PersonType.prototype.sayName
    sayName() {
        console.log(this.name);
    }
    
    //等价于PersonType.create
    static create(name) {
        return new PersonClass(name);
    }
}

let person = PersonClass.create('waltz');

类中的所有方法和访问器属性都可以用static关键字来定义,唯一的限制是不能将static用于定义构造函数方法。

不可在实例中访问静态成员,必须要直接在类中访问静态成员。

九、继承与派生类

ES5实现

function Rectangle(length, width) {
    this.length = length;
    this.width = width;
}

Rectangle.prototype.getArea = function() {
    return this.length * this.width;
};

function Square(length) {
    Rectangle.call(this, length, length);
}

Square.prototype = Object.create(Rectangle.prototype, {
    constuctor: {
        value: Square,
        enumerable: true,
        writable: true,
        configurable: true
    }
});

var square = new Square(3);

console.log(square.getArea()); // 9
console.log(square instanceof Square); //true
console.log(square instanceof Rectangle); true

ES6类实现

class Rectangle {
    constructor(length, width) {
        this.length = length;
        this.width = width;
    }
    
    getArea() {
        return this.length * this.width;
    }
}

class Square extends Rectangle {
    //派生类指定了构造函数则必须要调用 super()
    constructor(length) {
        
        //等价于Retangle.call(this, length, length)
        super(length, length);
    }
    
    //如果不使用构造函数,则当创建新的类实例时会自动调用 super() 并传入所有参数
}

var square = new Square(3);

console.log(square.getArea()); //9
console.log(square instanceof Square); //true
console.log(square instanceof Rectangle); //true

使用super()的小贴士:

  • 只可在派生类的构造函数中使用super(),如果尝试在非派生类(不是用extends声明的类)或函数中使用则会导致程序抛出错误。
  • 在构造函数中访问this之前一定要调用super(),它负责初始化this,如果在调用super()之前尝试访问this会导致程序错误。
  • 如果不想调用super(),则唯一的方法是让类的构造函数返回一个对象。

类方法遮蔽 --- 派生类中的方法总会覆盖基类中的同名方法。

静态成员继承 --- 如果基类有静态成员,那么这些静态成员在派生类中也可用。

派生自表达式的类 --- 只要表达式可以解析为一个函数并且具有[[Constructor]]属性和原型,那么就可以用extends进行派生。 extends强大的功能使得类可以继承自任意类型的表达式,从而创造更多可能性。

由于可以动态确定使用哪个基类,因而可以创建不同的继承方法

let SerializationMixin = {
    serialize() {
        return JSON.stringify(this);
    }
};

let AreaMixin = {
    getArea() {
        return this.length * this.width;
    }
};

function mixin(...mixins) {
    var base = function() {};
    Object.assign(base.prototype, ...mixins);
    return base;
}

class Square extends mixin(AreaMixin, SerializableMixin) {
    constructor(length) {
        super();
        this.length = length;
        this.width = length;
    }
}

var x = new Square(3);
console.log(x.getArea());  //9
console.log(x.serialize()); // "{'length': 3, 'width': 3}"

//如果多个mixin对象具有相同属性,那么只有最后一个被添加的属性被保留。

内建对象的继承

class MyArray extends Array {
    //空
}

var colors = new MyArray();
colors[0] = "red";

console.log(colors.length); //1
colors.length = 0;
console.log(colors[0]); //undefined

Symbol.species属性

内建对象继承的一个实用之处,原本在内建对象中返回实例自身的方法将自动返回派生类的实例。

Symbol.species是诸多内部Symbol中的一个,它被用于定义返回函数的静态访问器属性。被返回的函数是一个构造函数,每当要在实例的方法中(不是在构造函数中)创建类的实例时必须使用这个构造函数。

一般来说,只要想在类方法中调用this.constructor,就应该使用Symbol.species属性,从而让派生类重写返回类型。而且如果你正从一个已定义Symbol.species属性的类创建派生类,那么确保使用哪个值而不是使用构造函数。

class MyArray extends Array {
    static get [Symbol.species]() {
        return Array;
    }
}

let items = new MyArray(1, 2, 3, 4),
    subitems = items.slice(1, 3);
    
console.log(items instanceof MyArray); //true
console.log(subitems instanceof Array); //true
console.log(subitems instanceof MyArray); //false

十、在类的构造函数中使用new.target

在简单情况下,new.target等于类的构造函数。

因为类必须通过new关键字才能调用,所以在列的构造函数中,new.target属性永远不会是undefined

第十章 改进的数组功能

一、创建数组

1.1 ES6之前创建数组的方法:

  • 调用Array构造函数
  • 用数组字面量语法

1.2 ES6:

  • Array.of();

    作用:帮助开发者们规避通过Array构造函数创建数组是的怪异行为,因为,Array构造函数表现的与传入的的参数类型及数量有些不符;

    function createArray(arrayCreator, value){
        return arrayCreator(value);
    }
    
    let items = createArray(Array.of, value)
    
  • Array.from();

    ES5方法将类数组转化为真正的数组:

    function makeArray(arrayLike) {
        var result = [];
        for(var i=0, len = arrayLike.length; i<len; i++) {
            result.push(arrayLike[i]);
        }
        
        return result;
    }
    
    function doSomething() {
        var args = makeArray(arguments);
        
        //使用args
    }
    

    改进方法:

    function makeArray(arrayLike) {
        return Array.prototype.slice.call(arrayLike);
    }
    
    function doSomething() {
        var args = makeArray(arguments);
        
        //使用args
    }
    

    ES6-Array.from():

    function doSomething() {
        var args = Array.from(arguments);
        
        //使用args
    }
    

1.3 映射转换

如果想要进一步转化数组,可以提供一个映射函数作为Array.from()的第二个参数,这个函数用来将类数组对象中的每一个值转换成其他形式,最后将这些结果储存在结果数组的相应索引中。

function translate() {
    return Array.from(arguments, (value) => value + 1);
}

let numbers = translate(1, 2, 3);
console.log(numbers); //2, 3, 4

也可以传入第三个参数来表示映射函数的this值

let helper = {
    diff: 1,
    
    add(value) {
        return value + this.diff;
    }
};

function translate() {
    return Array.from(arguments, helper.add, helper);
}

let numbers = translate(1, 2, 3);
console.log(numbers); //2, 3, 4

Array.from()转换可迭代对象

let numbers = {
    *[Symbol.iterator]() {
        yield 1;
        yield 2;
        yield 3;
    }
};

let numbers = Array.from(numbers, (value) => value + 1);

console.log(numbers2); //2, 3, 4

如果一个对象既是类数组又是可迭代的,那么Array.from()方法会根据迭代器来决定转换那个值。

二、为所有数组添加的新方法

2.1 find()方法和findIndex()方法

一旦回调函数返回truefind()方法和findIndex()方法都会立即停止搜索数组剩余的部分。

适用于根据某个条件查找匹配的元素,如果只想查找与某个值匹配的元素,则indexOf()方法和lastIndexOf()方法是更好的选择。

2.2 fill()方法

  • 传入一个值,会用这个值重写数组中的所有值;
  • 传入第二个索引参数,表示从该索引位置开始替换;

如果开始索引或结束索引为负值,那么这些值会与数组的length属性相加来作为最终的位置。

2.3copyWith()方法

传入两个参数,一个是开始填充值的索引位置,另一个是开始复制值的索引位置。(如果索引存在负值,也会与数组的length属性相加作为最终值

三、定型数组

3.1 定义

定型数组可以为JavaScript带来快速的换位运算。ES6采用定型数组作为语言的正式格式来确保更好的跨JavaScript引擎兼容性以及与JavaScript数组的互操作性。

所谓定型数组,就是将任何数字转换为一个包含数字比特的数组。

定型数组支持存储和操作以下8种不同的数值类型:

  • 有符号的8位整数(int8)
  • 无符号的8位整数(uint8)
  • 有符号的16位整数(int16)
  • 无符号的16位整数(uint16)
  • 有符号的32位整数(int32)
  • 无符号的32位整数(uint32)
  • 32位浮点数(float32)
  • 64位浮点数(float64)

所有与定型数组有关的操作和对象都集中在这8个数据类型上,但是在使用它们之前,需要创建一个 数组缓冲区 存储这些数据。

3.2 数组缓冲区

数组缓冲区是所有定型数组的根基,它是一段可以包含特定数量字节的内存地址。(类似c语言malloc()分配内存)

let buffer = new ArrayBuffer(10); //分配10字节
console.log(buffer.byteLength); // 10

数组缓冲区包含的实际字节数量在创建时就已确定,可以修改缓冲区内的数据,但是不能改变缓冲区的尺寸大小。

DataView类型是一种通用的数组缓冲区视图,其支持所有8种数值型数据类型。

let buffer = new ArrayBuffer(10),
    view = new DataView(buffer);

可以基于同一个数组缓冲区创建多个view, 因而可以为应用申请一整块独立的内存地址,而不是当需要空间时再动态分配。

  • 获取读取试图信息

  • 读写视图信息

    • 视图是独立的,无论数据之前是通过何种方式存储的,你都可在任意时刻读取或写入任意格式的数据。
let buffer = new ArrayBuffer(2),
    view = new DataView(buffer); 

view.setInt(0, 5);
view.setInt(1, -1);

console.log(view.getInt16(0)); // 1535
console.log(view.getInt8(0)); //5
console.log(view.getInt8(1)); //-1
  • 定型数组是视图

    • ES6定型数组实际上是用于数组缓冲区的特定类型的视图,你可以强制使用特定的数据类型。而不是使用通用的DataView对象来操作数组缓冲区。
    • 创建定型数组的三种方法。

3.3 定型数组与普通数组的相似之处

可以修改length属性来改变普通数组的大小,而定型数组的length属性是一个不可写属性,所以不能修改定型数组的大小。

3.4 定型数组与普通数组的差别

定型数组和普通数组最重要的差别是:定型数组不是普通数组。

定型数组同样会检查数据类型的合法性,0被用于代替所以非法值。

  • 附加方法

set(): 将其它数组复制到已有的定型数组。

subarray(): 提取已有定型数组的一部分作为一个新的定型数组。

第十一章、Promise与异步编程

一、异步编程的背景知识

JavaScript既可以像事件和回调函数一样指定稍后执行的代码,也可以明确指示代码是否成功执行。

JavaScript引擎一次只能执行一个代码块,所以需要跟踪即将运行的代码,那些代码被放在一个任务队列中,每当一段代码准备执行时,都会被添加到任务队列。每当JavaScript引擎中的一段代码结束执行,事件循环(event loop) 会执行队列中的下一个任务,它是JavaScript引擎中的一段程序,负责监督代码执行并管理任务队列。

事件模型--->回调模式--->Promise

二、Promise的基础知识

Promise相当于异步操作结果的占位符,它不会去订阅一个事件,也不会传递一个回调函数给目标函数,而是让函数返回一个Promise对象。like:

// readFile承诺将在未来的某个时刻完成
let promise = readFile("example.txt");

操作完成后,Promise会进入两个状态:

  • Fulfilled Promise异步操作成功完成;
  • Rejected 由于程序错误或一些其他原因,Promise异步操作未能成功完成。

内部属性[[PromiseState]]被用来表示Promise的三种状态:"pending"、"fulfilled"、"rejected"。这个属性不暴露在Promise对象上,所以不能以编程的方式检测Promise的状态,只有当Promise的状态改变时,通过then()方法采取特定的行动。

如果一个对象实现了上述的then()方法,那这个对象我们称之为thenable对象。所有的Promise都是thenable对象,但并非所有thenable对象都是Promise

then方法

catch方法(相当于只给其传入拒绝处理程序的then()方法)

// 拒绝
promise.catch(function(err)) {
    console.error(err.message);
});

与下面调用相同

promise.then(null, function(err)){
    // 拒绝
    console.error(error.message);
});

Promise比事件和回调函数更好用

  • 如果使用事件,在遇到错误时不会主动触发;
  • 如果使用回调函数,则必须要记得每次都检查错误参数;
  • 不给Promise添加拒绝处理程序,那所有失败就自动被忽略了,所以一段要添加拒绝处理程序。

如果一个Promise处于已处理状态,在这之后添加到任务队列中的处理程序仍将进行。

三、创建未完成的Promise

Promise的执行器会立即执行,然后才执行后续流程中的代码:

let promise = new Promise(function(resolve, reject){
    console.log("Promise");
    resolve();
})

console.log("Hi!");

//Promise
//Hi!

完成处理程序和拒绝处理程序总是在 执行器 完成后被添加到任务队列的末尾。

四、创建已处理的Promise

  • 使用Promise.resolve()

  • 使用Promise.reject()

    如果向Promise.resolve()方法或Promise.reject()方法传入一个Promise, 那么这个Promise会被直接返回。

  • PromiseThenable对象

    Promise.resolve()方法和Promise.reject()方法都可以接受非PromiseThenable对象作为参数。如果传入一个非PromiseThenable对象,则这些方法会创建一个新的Promise,并在then()函数中被调用。

    PromiseThenable对象: 拥有then()方法并且接受resolvereject这两个参数的普通对象。

    如果不确定某个对象是不是Promise对象,那么可以根据预期的结果将其传入Promise.resolve()方法中或Promise.object()方法中,如果它是Promise对象,则不会有任何变化。

五、执行器错误

每个执行器都隐含一个try-catch块,所以错误会被捕获并传入拒绝处理程序。

六、全局的Promise拒绝处理

有关Promise的其中一个 最具争议 的问题是,如果在没有拒绝处理程序的情况下拒绝一个Promise,那么不会提示失败信息。

6.1 Node.js环境的拒绝处理

  • unhandledRejection

    在一个事件循环中,当Promise被拒绝,并且没有提供拒绝处理程序时,触发该事件。

    let rejected;
    
    process.on("unhandledRejection", function(reason, promise){
        console.log(reason.message); // "Explosion!"
        console.log(rejected === promise); // true
    });
    
    rejected = Promise.reject(new Error("Explosion!"));
    
  • rejectionHandled

    在一个事件循环之后,当Promise被拒绝时,若拒绝处理程序被调用,触发该事件。

    let rejected;
    
    process.on("rejectionHandled", function(promise){
        console.log(rejected === promise); // true
    });
    
    rejected = Promise.reject(new Error("Explosion!"));
    
    //等待添加拒绝处理程序
    setTimeout(function(){
        rejected.catch(function(value){
            console.log(value.message); // "Explosion!"
        });   
    }, 1000);
    

6.2 浏览器环境 的拒绝处理

  • unhandledRejection(描述与Node.js相同)

  • rejectionHandled

浏览器中的实现与Node.js中的几乎完全相同,二者都是用同样的方法将Promise及其拒绝值存储在Map集合中,然后再进行检索。唯一的区别是,在事件处理程序中检索信息的位置不同。

七、串联Promise

每次调用then()方法或catch()方法时实际上创建并返回了另一个Promise,只有当第一个Promise完成或被拒绝后,第二个才会被解决。

务必在Promise链的末尾留有一个拒绝处理程序以确保能够正确处理所有可能发生的错误。

拒绝处理程序中返回的值仍可用在下一个Promise的完成处理程序中,在必要时,即使其中一个Promise失败也能恢复整条链的执行。

八、在Promise中返回Promise

在完成或拒绝处理程序中返回Thenable对象不会改变Promise执行器的执行动机,先定义的Promise的执行器先执行,后定义的后执行。

九、响应多个Promise

  • Promise.All()方法
  • Promise.race()方法

十、自Promise继承

Promise与其他内建类型一样,也可以作为基类派生其他类,所以你可以定义自己的Promise变量来扩展内建Promise的功能。

十一、基于Promise的异步任务执行

let fs = require("fs");
function run(taskDef) {

    //创建迭代器
    let task = taskDef();
    
    //开始执行任务
    let result = task.next();
    
    //递归函数遍历
    (function step() {
    
        //如果有更多任务要做
        if(!result.done) {
            
            //用一个Promise来解决会简化问题
            let promise = Promise.resolve(result.value);
            promise.then(function(value) {
                result = task.next(value);
                step();
            }).catch(function(error){
                result = task.throw(error);
                step();
            })
        }
    }());
}

//定义一个可用于任务执行器的函数

function readFile(filename) {
    return new Promise(function(resolve, reject) {
       fs.readFile(filename, function(err, contents){
            if(err){
                reject(err);
            }else{
                resolve(contents);
            }
       }); 
    });
}

//执行一个任务

run(function*(){
    let contents = yield readFile("config.json");
    doSomethingWith(contents);
    console.log("done");
})

ES2017 await

第十二章、代理(Proxy)和反射(Reflection)API

代理(Proxy)是一种可以拦截并改变底层JavaScript引擎操作的包装器,在新语言中通过它暴露内部运作的对象。

一、代理和反射

调用 new Proxy()可创建代替其他目标对象的代理,它虚拟化了目标,所以二者看起来功能一致。

代理可以拦截 JavaScript 引擎内部目标的底层对象操作,这些底层操作被拦截后会触发响应特定操作的陷阱函数。

反射APIReflect对象的形式出现,对象中方法的默认特性与相同的底层操作一致,而代理可以覆写这些操作,每个代理陷阱对应一个命名和参数都相同的Reflect方法。

二、使用set陷阱验证属性 / 用get陷阱验证对象解构(Object Shape)

set代理陷阱可以拦截写入属性的操作,get代理陷阱可以拦截读取属性的操作。

let target = {
    name: "target"
}

let proxy = new Proxy(target, {
    set(trapTarget, key, value, receiver) {
        
        //忽略不希望受到影响的已有属性
        if(!trapTarget.hasOwnProperty(key)) {
            if(isNaN(value)) {
                throw new TypeError("属性必须是数字");
            }
        }
        
        //添加属性
        return Reflect.set(trapTarget, key, value, receiver);
    }
});

//添加一个新属性
proxy.count = 1;
console.log(proxy.count); //1
console.log(target.count); //1

//由于目标已有name属性因而可以给它赋值
proxy.name = "proxy";
console.log(proxy.name); //"proxy"
console.log(target.name); //"proxy"
let proxy = new Proxy({},{
    get(trapTarget, key, receiver) {
        if (!(key in receiver)) {
            throw new TypeError("属性" + key + "不存在");
        }
        
        return Reflect.get(trapTarget, key, receiver);
    }
});

//添加一个属性,程序仍正常运行
proxy.name = "proxy";
console.log(proxy.name); // "proxy"

//如果属性不存在,则抛出错误
console.log(proxy.nme); // 抛出错误

三、使用has陷阱隐藏已有属性

四、使用deleteProperty陷阱防止删除属性

五、原型代理陷阱

  • 原型代理陷阱的运行机制

原型代理陷阱有一些限制:

  1. getPrototypeOf陷阱必须返回对象或null,只要返回值必将导致运行时错误,返回值检查可以确保Object.getPropertyOf()返回的总是预期的值;
  2. setPropertyOf陷阱中,如果操作失败则返回的一定是false,此时Object.setPrototypeOf()会抛出错误,如果setPrototypeOf返回了任何不是false的值,那么Object.setPrototypeOf()便假设操作成功。
  • 为什么有两组方法

Object.getPrototypeOf()Object.setPrototypeOf()是高级操作,创建伊始便给开发者使用的;而Reflect.getPrototypeOf()Reflect.setPrototypeOf()方法则是底层操作,其赋予开发者可以访问之前只在内部操作的[[GetPrototypeOf]][[SetPrototypeOf]]的权限。

Object.setPrototypeOf()Reflect.setPrototypeOf()之间在返回值上有微妙的差异,前者返回传入的对象,后者返回布尔值。

六、对象可扩展性陷阱

  • preventExtensions(阻止扩展)
  • isExtensible(判断是否可扩展)

相比高级功能方法而言,底层的具有更严格的错误检查。

七、属性描述符陷阱

  • defineProperty(定义属性)
  • getOwnPropertyDescriptor(获取属性)

给Object.defineProperty()添加限制

如果让陷阱返回true并且不调用Reflect.defineProperty()方法,则可以让Object.definePropperty()方法静默失效,这既消除了错误又不会真正定义属性。

描述符对象限制

defineProperty陷阱被调用时,descriptor对象有value属性却没有name属性,这是因为descriptor不是实际传入Object.defineProperty()方法的第三个参数的引用,而是一个只包含那些被允许使用的属性的新对象。Reflect.defineProperty()方法同样也忽略了描述符上的所有非标准属性。

八、ownKeys陷阱

ownKeys陷阱通过Reflect.ownKeys()方法实现默认的行为,返回的数组中包含所有自有属性的键名,字符串类型和Symbol类型的都包含在内。

  • Object.getOwnPropertyNames()方法和Object.keys()方法返回的结果将Symbol类型的属性名排除在外。
  • Object.getOwnPropertySymbols()方法返回的结果将字符串类型的属性名排除在外。
  • Object.assign()方法支持字符串和Symbol两种类型。

九、函数代理中的applyconstruct陷阱

所有的代理陷阱中,只有applyconstruct的代理目标是一个函数。

  • 验证函数参数
  • 不用new调用构造函数

可以通过检查new target的值来确定函数是否是通过new来调用的。

假设Numbers()函数定义在你无法修改的代码中,你知道代码依赖new target,希望函数避免检查却仍想调用函数。在这种情况下,用new调用时的行为已被设定,所以你只能使用apply陷阱。

  • 覆写抽象基类构造函数
  • 可调用的类构造函数

十、可撤销代理

十一、解决数组问题

  • 检测数组索引
  • 添加新元素时增加length的值
  • 减少length的值来删除元素

十二、实现MyArray类

想要创建使用代理的类,最简单的方法是像往常一样定义类,然后在构造函数中返回一个代理,那样的话,当类实例化时返回的对象是代理而不是实例(构造函数中的this是该实例)。

将代理用作原型

虽然从类构造函数返回代理很容易,但这也意味着每创建一个实例都要创建一个新代理。然而有一种方法可以让所有的实例共享一个代理:将代理用作原型。

  • 在原型上使用get陷阱
  • 在原型上使用set陷阱
  • 在原型上使用has陷阱
  • 将代理用作类的原型

第十三章 用模块封装代码

一、什么是模块?

模块是自动运行在严格模式下并且没有办法退出运行的Javascript代码。

注:在模块的顶部,this的值是undefined;模块不支持HTML风格的代码注释。

  • 导出的基本语法
  • 导入的基本语法
    • 导入单个绑定
    • 导入多个绑定导入

绑定的微妙怪异之处

export var name = "xszi";
export function setName(newName) {
    name = newName;
}

//导入之后
import { name, setName } from "./example.js";

console.log(name);  //xszi
setName("waltz");
console.log(name); //waltz

name = "hahha"; //抛出错误

  • 导入和导出重命名

  • 模块的默认值

    • 导出默认值
    • 导入默认值

    只能为每个模块设置一个默认的导出值,导出时多次使用default关键字是一个语法错误。

    用逗号将默认的本地名称与大括号包裹的非默认值分隔开,请记住,在import语句中,默认值必须排在非默认值之前。

  • 重新导出一个绑定

  • 无绑定导入

即使没有任何导出或导入的操作,这也是一个有效的模块。

无绑定导入最有可能被应用与创建PilyfillShim

Shim: 是一个库,它将一个新的API引入到一个旧的环境中,而且仅靠旧环境中已有的手段实现。

Polyfill: 一个用在浏览器API上的Shim,我们通常的做法是先检查当前浏览器是否支持某个API,如果不支持的话就加载对用的polyfill

把旧的浏览器想想成一面有裂缝的墙,这些polyfill会帮助我们把这面墙的裂缝填平。

二、加载模块

  1. 在web浏览器中使用模块

    //加载一个JavaScript模块文件
    <script type="module" src="module.js"></script>
    
    //内联引入模块
    <script type="module">
    import { sum } from "./example.js";
    let result = sum(1, 2)
    </script>
    
    • web浏览器中的模块加载顺序

    模块与脚本不同,它是独一无二的,可以通过import关键字来指明其所依赖的其他文件,并且这些文件必须被加载进该模块才能正确执行。为了支持该功能,<script type="module">执行时自动应用defer属性。

    每个模块都可以从一个或多个其他的模块导入,这会使问题复杂化。因此,首先解析模块以识别所有导入语句;然后,每个导入语句都触发一次获取过程(从网络或从缓存),并且在所有导入资源都被加载和执行后才会执行当前模块。

    • web浏览器中的异步模块加载
    //无法保证这两个哪个先执行
    <script type="module" async src="module1.js"></script>
    <script type="module" async src="module2.js"></script>
    

    将模块作为Worker加载

    Worker可以在网页上下文之外执行JavaScript代码。

    //按照脚本的方式加载script.js
    let worker = new Worker("script.js");
    
    //按照模块的方式加载module.js
    let worker = new Worker("module.js", {type: "module"});
    

A ECMAScript6中较小的改动

一、 安全整数

IEEE 754只能准确的表示-2^53 ~ 2^53之间的整数:

var inside = Number.MAX_SAFE_INTEGER,
    outside = inside + 1;

console.log(Number.isInteger(inside)); //true
console.log(Number.isSafeInteger(inside)); //true

console.log(Number.isInteger(outside)); //true
console.log(Number.isSafeInteger(outside)); //false

二、 新的Math方法

提高通常的数学计算的速度

三、 Unicode 标识符

四、正式化_ptoto_属性

实际上,_proto_Object.getPrototypeOf()方法和Object.setPrototypeOf()方法的早期实现。

B 了解ECMAScript 7 (2016)

一、指数运算符

5 ** 2 == Math.pow(5, 2); //true

求幂运算符在JavaScript所有二进制运算符中具有最高的优先级(一元运算符的优先级高于**)

二、Array.prototype.includes()方法

奇怪之处:

includes()方法认为+0和-0是相等的,Object.is()方法会将+0和-0识别为不同的值。

三、函数作用域严格模式的一处改动

ECMAScript 2016 规定在参数被解构或有默认参数的函数中禁止使用"use strict"指令。只有参数为不包含解构或默认值的简单参数列表才可以在函数体中使用"use strict"