深刻理解ES6

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

第一章 块级做用域绑定

var 声明初始化变量, 声明能够提高,但初始化不能够提高。html

1、块级声明:

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

2、临时死区:

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

第一种状况:es6

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

第二种状况:web

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

3、循坏中块做用域绑定

  • 当即调用(IIFE)正则表达式

  • letconst之因此能够在运用在for-infor-of循环中,是由于每次迭代会建立一个新的绑定(const在for循环中会报错)。编程

4、全局块做用域绑定

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

注:若是但愿在全局对象下定义变量,仍然可使用var。这种状况常见于在浏览器中跨frame或跨window访问代码数组

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

1、UTF-8码位

名词解释:promise

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

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

2、codePointAt()、 fromCodePoint() 和 normalize()

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

3、正则表达式 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`

//代码模拟(略)
复制代码

第三章 函数

1、默认参数值

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);
})
复制代码

2、默认参数值对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; //处于临时死区
复制代码

3、不定参数的使用限制

  1. 每一个函数最多只能声明一个不定参数,并且必定要放在全部参数的末尾。
  2. 不定参数不能用于对象字面量setter之中(由于对象字面量setter的参数有且只有一个,而在不定参数的定义中,参数的数量能够无限多

不管是否使用不定参数,arguments对象老是包含全部传入函数的参数。

4、展开运算符

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

5、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属性的值来获取对于函数的引用。

6、明确函数的多重用途

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

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

7、元属性(Metaproperty)new.target

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

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

8、块级函数

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

9、箭头函数

箭头函数与传统的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对象

10、尾调用优化

  • 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);
    //不建立新的栈帧,而是消除并重用当前栈帧
}
复制代码

第四章 扩展对象的功能性

1、对象的类别

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

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

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

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

//二者惟一的区别是,简写方式可使用super关键字
复制代码

2、可计算属性名(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
复制代码

3、新增方法

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
复制代码

4、自有属性枚举顺序

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

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

5、加强对象的原型

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

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

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

6、正式的方法定义

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

Super的因此引用都经过 [[HomeObject]] 属性来肯定后续的运行过程:

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

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

1、对象解构

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必定须要;
复制代码

2、解构赋值

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

  • 嵌套对象解构

3、数组解构

  • 解构赋值

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

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能够为属性添加非字符串名称,可是其隐私性就被打破了。

1、建立、使用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.

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

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

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

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

全部对象一开始没有本身独有的属性,可是对象能够从原型链中继承Symbol属性。

3、经过well-know Symbol暴露内部操做

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

第七章 Set集合与Map集合

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

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

1、用对象属性模拟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集合常被用来获取已存的信息。

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

2、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]
复制代码

3、 Weak Set 集合

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

Weak Set集合只存储对象的弱引用,而且不能够存储原始值;集合中的弱引用若是是对象惟一的引用,则会被回收并释放相应内存。

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

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

4、 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集合中以前不会被强制转换为其余数据类型,于是只能将它们放在数组中,由于这是惟一一种能够准确地呈现键名类型的方式。

5、 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)

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

1、什么是迭代器?

迭代器是一种特殊对象,他具备一些专门为迭代过程设计的专有端口,全部的迭代器对象都有一个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}"
复制代码

2、什么是生成器?

生成器是一种返回迭代器的函数,经过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关键字同样,不能穿透函数的边界。

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

3、可迭代对象和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
复制代码

4、内建迭代器

  • 集合对象迭代器

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

  • NodeList迭代器

5、高级迭代器功能

  • 给迭代器传递参数
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

复制代码

1、 类的声明

  • 基本的类声明方法
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'
复制代码

自有属性是实例中的属性,不会出如今原型上,且只能在类的构造函数或方法中建立。建议你在构造函数中建立全部的自有属性,从而只经过一处就能够控制类中的全部自有属性。

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

2、 为什么使用类语法

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

3、类表达式

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

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

4、做为一等公民的类

在程序中。一等公民是指一个能够传入函数,能够从函数返回,而且能够赋值给变量的值。(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'

复制代码

依照这种模式可使用类语法建立单例,而且不会再做用域中暴露类的引用。

5、访问器属性

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
复制代码

6、可计算成员名称

//类方法
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;
    }
}
复制代码

7、生成器方法

class MyClass {

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

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

复制代码

若是用对象来表示集合,又但愿经过简单的方法迭代集合中的值,那么生成器方法就派上用场了。

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

8、静态成员

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

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用于定义构造函数方法。

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

9、继承与派生类

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
复制代码

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

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

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

第十章 改进的数组功能

1、建立数组

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、为全部数组添加的新方法

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

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

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

2.2 fill()方法

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

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

2.3copyWith()方法

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

3、定型数组

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与异步编程

1、异步编程的背景知识

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

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

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

2、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处于已处理状态,在这以后添加到任务队列中的处理程序仍将进行。

3、建立未完成的Promise

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

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

console.log("Hi!");

//Promise
//Hi!
复制代码

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

4、建立已处理的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对象,则不会有任何变化。

5、执行器错误

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

6、全局的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集合中,而后再进行检索。惟一的区别是,在事件处理程序中检索信息的位置不一样。

7、串联Promise

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

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

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

8、在Promise中返回Promise

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

9、响应多个Promise

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

10、自Promise继承

Promise与其余内建类型同样,也能够做为基类派生其余类,因此你能够定义本身的Promise变量来扩展内建Promise的功能。

11、基于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引擎操做的包装器,在新语言中经过它暴露内部运做的对象。

1、代理和反射

调用 new Proxy()可建立代替其余目标对象的代理,它虚拟化了目标,因此两者看起来功能一致。

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

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

2、使用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); // 抛出错误
复制代码

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

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

5、原型代理陷阱

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

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

  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()之间在返回值上有微妙的差别,前者返回传入的对象,后者返回布尔值。

6、对象可扩展性陷阱

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

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

7、属性描述符陷阱

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

给Object.defineProperty()添加限制

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

描述符对象限制

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

8、ownKeys陷阱

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

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

9、函数代理中的applyconstruct陷阱

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

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

能够经过检查new target的值来肯定函数是不是经过new来调用的。

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

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

10、可撤销代理

11、解决数组问题

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

12、实现MyArray类

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

将代理用做原型

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

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

第十三章 用模块封装代码

1、什么是模块?

模块是自动运行在严格模式下而且没有办法退出运行的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会帮助咱们把这面墙的裂缝填平。

2、加载模块

  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中较小的改动

1、 安全整数

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
复制代码

2、 新的Math方法

提升一般的数学计算的速度

3、 Unicode 标识符

4、正式化_ptoto_属性

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

B 了解ECMAScript 7 (2016)

1、指数运算符

5 ** 2 == Math.pow(5, 2); //true
复制代码

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

2、Array.prototype.includes()方法

奇怪之处:

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

3、函数做用域严格模式的一处改动

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

相关文章
相关标签/搜索