目录
八,迭代器(lterator)和生成器(generator)
var
使用var声明的变量,无论是在代码的哪个地方声明的,都会提升到当前作用域的最顶部,这种行为叫做变量提升(Hoisting)
如果在函数内部声明的变量,都会被提升到该函数开头,而在全局声明的变量,就会提升到全局作用域的顶部。
在函数嵌套函数的场景下,变量只会提升到最近的一个函数顶部,而不会提升到外部函数。
let
let和const都能够声明块级作用域,用法和var是类似的,let的特点是不会变量提升,而是被锁在当前块中。
禁止重复声明
const
声明常量,一旦声明,不可更改,而且常量必须初始化赋值。
const不允许修改绑定,单允许修改值,所以声明对象Object,是可以修改对象内部的属性值
const text = {
a: 1
}
text.a = 2 //没有直接修改text的值,而是修改text.a的属性值,这是允许的。
const和let的异同点
相同点:const和let都是在当前块内有效,执行到块外会被销毁,也不存在变量提升,不能重复声明。
不同点:const不能再赋值,let声明的变量可以重复赋值。
临时死区(Temporary Dead Zone)
临时死区的意思是在当前作用域的块内,在声明变量前的区域叫做临时死区。
if (true) {
//这块区域是TDZ
let a = 1
}
循环中块级作用域的绑定
在for循环中使用var声明的循环变量,会跳出循环体污染当前的函数。
for(var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i) //5, 5, 5, 5, 5
}, 0)
}
console.log(i) //5 i跳出循环体污染外部函数
for(let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i) // 0,1,2,3,4
}, 0)
}
console.log(i)//i is not defined i无法污染外部函数
在全局作用域声明
如果在全局作用域使用let或者const声明,当声明的变量本身就是全局属性,比如closed。只会覆盖该全局变量,而不会替换它。
window.closed = false
let closed = true
closed // true
window.closed // false
最佳实践
在实际开发中,我们选择使用var、let还是const,取决于我们的变量是不是需要更新,通常我们希望变量保证不被恶意修改,而使用大量的const,在react中,props传递的对象是不可更改的,所以使用const声明,声明一个对象的时候,也推荐使用const,当你需要修改声明的变量值时,使用let,var能用的场景都可以使用let替代。
二,字符串和正则表达式
字符串(String)是JavaScript6大原始数据类型。其他几个分别是Boolean、Null、Undefined、Number、Symbol(es6新增)。
更好的Unicode支持
当Unicode引入扩展字符集之后,16位的字符已经不足以满足字符串的发展,所以才在ES6中更新了Unicode的支持。
UTF-16码位:
ES6强制使用UTF-16字符串编码
codePointAt():
该方法支持UTF-16,接受编码单元的位置而非字符串位置作为参数,返回与字符串中给定位置对应的码位,即一个整数值。
let a='吉a' console.log(a.charCodePointAt(0)) //134071 console.log(a.charCodePointAt(1)) //57271 console.log(a.charCodePointAt(2)) //97
String.fromCodePoiont():
作用与codePointAt相反,可以根据指定的码位生成一个字符。
normalize():
提供Unicode的标准形式,接受一个可选的字符串参数,指明应用某种Unicode标准形式。
正则表达式
正则表达式u修饰符: 当给正则表达式添加u字符时,它就从编码单元操作模式切换为字符模式。
检测
function hasReg(){ try { var pattern=new RegExp(".","u") return true; } catch(ex){ return false } }
其他字符串变更
字符串中的子串识别:
以前我们使用indexOf()来检测字符串中是否包含另外一段字符串。
let t = 'abcdefg' if(t.indexOf('cde') > -1) { console.log(2) } //输出2,因为t字符串中包含cde字符串。
在ES6中,新增了3个新方法。每个方法都接收2个参数,需要检测的子字符串,以及开始匹配的索引位置。
includes(str, index):如果在字符串中检测到指定文本,返回true,否则false。
let t = 'abcdefg' if(t.includes('cde')) { console.log(2) } //true
startsWith(str, index):如果在字符串起始部分检测到指定文本,返回true,否则返回false。
let t = 'abcdefg' if(t.startsWith('ab')) { console.log(2) } //true
endsWith(str, index):如果在字符串的结束部分检测到指定文本,返回true,否则返回false。
let t = 'abcdefg' if(t.endsWith('fg')) { console.log(2) } //true
如果你只是需要匹配字符串中是否包含某子字符串,那么推荐使用新增的方法,如果需要找到匹配字符串的位置,使用indexOf()。
repeat(number)
接收一个Number类型的数据,返回一个重复N次的新字符串。即使这个字符串是空字符,也你能返回N个空字符的新字符串。
console.log('ab'.repeat(3)) //ababab
正则表达式的其他更新
正则表达式 y 修饰符:该属性会通知搜索正则表达式的 lastIndex 属性开始进行,如果在指定位置没有匹配成功,则停止继续匹配。
flags属性:该属性可以返回所有应用于当前正则表达式的修饰符字符串。
模板字面量
模板字面量反撇号``
let a = `123`
在字符串中使用反撇号,只需要加上转义符。
let a = `12\`3` //字符串内插入反撇号的方式。 12`3
在多行字符串的使用价值:
不使用模板字面量,实现多行字符串,会使用换行符/n。
let a = '123\n456' console.log(a) // 123 // 456
使用模板字面量
let a = `123 456 ` console.log(a) // 123 // 456
字符串占位符 在模板字面量插入变量的方法。
使用${params}直接插入要添加到字符串的位置。
let t = 'haha' let a = `hello,${t}` console.log(a) //hello,haha
标签模板 tag是一个方法,方法名你可以任意命名,每个标签都可以执行模板字面量上的转换并返回最终的字符串值。简单说,它就是一个包含了反撇号表达式的函数。这个函数以反撇号表达式作为参数,然后标签名就是这个函数的函数名
function tag(literals, ...substitutions) { //literals是数组,第一个位置是"",第二个位置是占位符之间的字符串,在本例中是haha //substitutions是字符串中的模板字面量,可能多个 //函数最终返回字符串 } let a = 4 let t = tag`${a} haha` console.log(t) //4 haha
三,函数
函数的默认参数
在ES5中,在函数体内要对形参的值进行进一步的判断,如果不满足条件,则给其一个默认值。例如:
function makeRequest(url, timeout, callback) { timeout = timeout || 2000; //other code }
而在ES6中,为了更加简化函数体的代码,给形参赋默认值的操作可以直接在写参数的时候加上,以此减少函数体内的代码量。例如:
function makeRequest(url, timeout = 2000, callback) { //other code }
使用ES6的默认值写法可以让函数体内部的代码更加简洁优雅
默认值对arguments对象的影响
arguments对象是是一个类数组对象,它存在函数内部,它将当前函数的所有参数组成了一个类数组对象。
修改参数默认值对arguments的影响。
1、在ES5的非严格模式下,一开始输入的参数是1,那么可以获取到arguments[0](表示第一个参数)全等于num,修改num = 2之后,arguments[0]也能更新到2。
function a(num){ console.log(num === arguments[0]) //true num = 2 //修改参数默认值 console.log(num === arguments[0]) //true } a(1)
2、在ES5的严格模式下,arguments就不能在函数内修改默认值后跟随着跟新了。
"use strict"; //严格模式 function a(num) { console.log(num === arguments[0]); // true num = 2; console.log(num === arguments[0]); // false } a(1);
在ES6环境下,默认值对arguments的影响和ES5严格模式是同样的标准。
默认参数表达式
参数不仅可以设置默认值为字符串,数字,数组或者对象,还可以是一个函数。
function add() { return 10 } function a(num = add()){ console.log(num) } a() // 10
默认参数的临时死区
第一章提到了let和const的临时死区(TDZ),默认参数既然是参数,那么也同样有临时死区,函数的作用域是独立的,a函数不能共享b函数的作用域参数。
//这是个默认参数临时死区的例子,当初始化a时,b还没有声明,所以第一个参数对b来说就是临时死区。 function add(a = b, b){ console.log(a + b) } add(undefined, 2) // b is not define
无命名参数
上面说的参数都是命名参数,而无命名参数也是函数传参时经常用到的。当传入的参数是一个对象,不是一个具体的参数名,则是无命名参数。
function add(object){ console.log(object.a + object.b) } let obj = { a: 1, b: 2 } add(obj) // 3
不定参数:使用...(展开运算符)的参数就是不定参数,它表示一个数组。
function add(...arr){ console.log(a + b) } let a = 1,b = 2 add(a, b) // 3
不定参数的使用限制:必须放在所有参数的末尾,不能用于对象字面量setter中。
//错误的写法1 function add(...arr, c){ console.log(a + b) } let a = 1,b = 2,c = 3 add(a, b, c) //错误的写法2 let obj = { set add(...arr) { } }
增强的Function构造函数
在ES5中,可以像下面这样使用构造函数:
var add = new Function( "first", "second", "return first + second" ); console.log( add(1, 1) ); // 2
在ES6中,Function构造函数可以使用默认参数和不定参数。例如:
var add = new Function("first", "second=first", "return first + second"); console.log( add(1, 1) ); //2 console.log( add(1) ); //2 var pickFirst = new Function( "...args", "return args[0]" ); console.log( pickFirst(1, 2) ); // 1
展开运算符(...)
展开运算符的作用是解构数组,然后将每个数组元素作为函数参数。
有了展开运算符,我们操作数组的时候,就可以不再使用apply来指定上下文环境了。
//ES5的写法 let arr = [10, 20, 50, 40, 30] let a = Math.max.apply(null, arr) console.log(a) // 50 //ES6的写法 let arr = [10, 20, 50, 40, 30] let a = Math.max(...arr) console.log(a) // 50
name属性
JavaScript中,有多种定义函数的方式。例如:正常定义的函数、函数表达式、匿名函数等。为了便于调试函数,于是在ES6中,添加了函数的 name 属性。该属性的值可以返回当前的函数名。
当然也有一些特殊的情况,会在函数名的前面加上一些字符串前缀。例如:
getter函数,函数名前面会有get
setter函数,函数名前面会有set
bind()函数,函数名前面会有bound
注意,函数name属性不一定和原函数名完全相同,所以它只是一个调试的辅助信息,而不能用它来获取对函数的引用
明确函数的多种用途
JavaScript函数有两个不同的内部方法:[[ Call ]] 、 [[ Construct ]]。
当通过 new 关键字调用函数的时候,执行的是 [[ Construct ]] 函数。如果不是通过 new 关键字来调用函数,那么执行的就是 [[ Call ]] 函数。
不是所有函数都有 [[ Construct ]]方法。
例如ES6中的箭头函数就没有这个[[ Condtruct ]]方法。
在ES6中,可以通过 new.target 这个属性来判断是否是通过 new 关键字调用的函数。
块级函数
严格模式下:在ES6中,你可以在块级作用域内声明函数,该函数的作用域只限于当前块,不能在块的外部访问。
"use strict"; if(true) { const a = function(){ } }
非严格模式:即使在ES6中,非严格模式下的块级函数,他的作用域会被提升到父级函数的顶部。
箭头函数(=>)
const arr = [5, 10] const s = arr.reduce((sum, item) => sum + item) console.log(s) // 15
箭头函数和普通函数的区别是:
1、箭头函数没有this,函数内部的this来自于父级最近的非箭头函数,并且不能改变this的指向。
2、箭头函数没有super
3、箭头函数没有arguments
4、箭头函数没有new.target绑定。
5、不能使用new
6、没有原型
7、不支持重复的命名参数。
箭头函数的简单理解
1、箭头函数的左边表示输入的参数,右边表示输出的结果。
const s = a => a console.log(s(2)) // 2
2、箭头函数中,没有this绑定。
3、箭头函数还可以输出对象,在react的action中就推荐这种写法。
const action = (type, a) => ({ type: "TYPE", a })
4、支持立即执行函数表达式写法
const test = ((id) => { return { getId() { console.log(id) } } })(18) test.getId() // 18
5、箭头函数给数组排序
const arr = [10, 50, 30, 40, 20] const s = arr.sort((a, b) => a - b) console.log(s) // [10,20,30,40,50]
尾调用优化
当一个函数作为另一个函数的最后一条语句被调用时,就叫做尾调用
ES6中,引擎会帮你做好尾调用的优化工作,但需要满足下面3个要求:
1、函数不是闭包
2、尾调用是函数最后一条语句
3、尾调用结果作为函数返回
一个满足以上要求的函数如下所示:
"use strict"; function a() { return b(); }
下面的都是不满足的写法:
//没有return不优化 "use strict"; function a() { b(); } //不是直接返回函数不优化 "use strict"; function a() { return 1 + b(); } //尾调用是函数不是最后一条语句不优化 "use strict"; function a() { const s = b(); return s } //闭包不优化 "use strict"; function a() { const num = 1 function b() { return num } return b }
尾调用实际用途——递归函数优化
//新型尾优化写法 "use strict"; function a(n, p = 1) { if(n <= 1) { return 1 * p } let s = n * p return a(n - 1, s) } //求 1 x 2 x 3的阶乘 let sum = a(3) console.log(sum) // 6
四,扩展对象的功能性
对象类别
在ES6中,对象分为下面几种叫法。
1、普通对象:具有JavaScript对象所有的内部默认行为
2、特异对象:具有某些与默认行为不符的内部行为
3、标准对象:es6规范定义的对象,array,data等,既可以是普通对象,也可以是特异对象
4、内建对象:脚本开始执行是存在于JavaScript执行环境中的对象,所以的标准对象都是内建对象
对象字面量语法拓展
ES6针对对象的语法扩展了一下功能
1、属性初始值简写
//ES5 function createPerson(name, age) { return { name: name, age: age } } //ES6 function createPerson(name, age) { return { name, age } }
2、对象方法简写
// ES5 var person = { name: 'foo', sayName: function() { console.log(this.name); } } // ES6 var person = { name: "foo", sayName() { console.log(this.name); } }
3、属性名可计算
属性名可以传入变量或者常量,而不只是一个固定的字符串。
const id = 5 const obj = { [`my-${id}`]: id } console.log(obj['my-5']) // 5
ES6对象新增方法
在Object原始对象上新增方法,原则上来说不可取,但是为了解决全世界各地提交的issue,在ES6中的全局Object对象上新增了一些方法。
1、Object.is()
该方法接收两个参数,判断它们是否相等。与全等“===”类似。但是有两个特例:
1)参数:+0 和 -0
console.log(+0 === -0); //true console.log(Object.is(+0, -0)); //false
2) 参数:NaN
console.log(NaN === NaN); //false console.log(Object.is(NaN, NaN)); //true
2、Object.assign()
共2个参数,将第2个参数复制到第1个参数中。
使用Object.assign(),你就可以不是有继承就能获得另一个对象的所有属性,快捷好用。 Object.assign 方法只复制源对象中可枚举的属性和对象自身的属性。 看一个实现Component的例子。
const target = { a: 1, b: 2 }; const source = { b: 4, c: 5 }; const returnedTarget = Object.assign(target, source); console.log(target); // expected output: Object { a: 1, b: 4, c: 5 } console.log(returnedTarget); // expected output: Object { a: 1, b: 4, c: 5 }
重复的对象字面量属性
ES5的严格模式下,如果你的对象中出现了key相同的情况,那么就会抛出错误。而在ES6的严格模式下,不会报错,后面的key会覆盖掉前面相同的key。
const state = { id: 1, id: 2 } console.log(state.id) // 2
自有属性枚举顺序
在ES5中没有定义对象属性的枚举顺序,在ES6中,严格规定了对象的自有属性被枚举时的返回顺序。
自有属性枚举顺序的规则:
1)所有数字键按升序排序;
2)所有字符串键按照它们被加入对象的顺序排序;
3)所有symbol键按它们被加入对象的顺序排序。
var obj = { a: 1, 0: 1, c: 1, 2: 1, b: 1, 1: 1 } obj.d = 1; console.log( Object.getOwnPropertyNames(obj).join("") ); // "012adcd" getOwnPropertyNames 返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组。
注意:1)数值键在枚举时会被重新组合和排序;
2)先枚举数字,再枚举字符串
增强对象原型
如果你想定义一个对象,你会想到很多方法。
let a = {} let b = Object.create(a) function C() {} class D {}
为了让开发者获得更多对原型的控制力,于是ES6对原型进行了修改。
改变原型对象。
在ES6中,添加了Object.setPropertyOf()方法来改变原型的对象及替代第一个参数原型的对象。
let a = { name() { return 'eryue' } } let b = Object.create(a) console.log(b.name()) // eryue //使用setPrototypeOf改变b的原型 let c = { name() { return "aa" } } Object.setPrototypeOf(b, c) console.log(b.name()) //aa
方法的定义
即在对象的内部的函数叫方法,没有在对象内部的函数叫函数。例如:
let a = { //方法 name() { return 'eryue' } } //函数 function name() {}
五,解构:使数据访问更便捷
解构是从对象中提取出更小元素的过程。赋值是对解构出来的元素进行重新赋值。
解构的分类
1、对象解构
2、数组解构
3、混合解构
4、解构参数
对象解构
对象解构的语法是在一个赋值操作符的左边,设置一个对象字面量
let obj = { a: 1, b: [1, 2] } // 对象解构 const { a, b } = obj console.log(a, b) //1 [1, 2]
默认值。
对解构赋值中不存在的属性可以随意定义一个默认值。举例:
let node = { type: "Identifier", name: "foo" } let {type, name, value=true} = node; console.log(type); //"Identifier" console.log(name); //"foo" console.log(value); //true
为非同名局部变量赋值。
如果希望用不同命名的局部变量来存储对象属性的值,可以用这样一个扩展语法来实现。举例:
let node = { type: "Identifier", name: "foo" } let {type:localType, name:localName} = node; console.log(localType); //"Identifier" console.log(localName); //"foo"
嵌套对象解构。
解构嵌套对象与对象字面量的语法相似。可以拆解以获取想要的信息。举例:
let node = { type: "Identifier", name: "foo", loc: { start: { line: 1, column: 1 }, end: { line: 1, column: 4 } } }; let { loc: {start} } = node; console.log(start.line); //1 console.log(start.column); // 1
数组解构
数组解构比对象解构简单,因为数组只有数组字面量,不需要像对象一个使用key属性。
数组解构 你可以选择性的解构元素,不需要解构的元素就使用逗号代替。
let arr = [1, 2, 3] //解构前2个元素 const [a, b] = arr console.log(a,b) //1 2 //解构中间的元素 const [, b,] = arr console.log(b) // 2
解构赋值
//初始化一个变量a let a = "haha"; //定义一个数组 let arr = [1, 2, 3]; //解构赋值a,将arr数组的第一个元素解构赋值给a, [a] = arr; console.log(a); // 1
使用解构赋值,还可以调换2个变量的值。
let a = 1, b = 2; [a, b] = [b, a]; console.log(a, b); // 2 1
嵌套数组解构
let arr = [1, [2, 3], 4]; let [a, [,b]] = arr; console.log(a, b) // 1 3 //实际解构过程,左边的变量和右边的数组元素一一对应下标。 var a = arr[0], _arr$ = arr[1], b = _arr$[1];
不定元素解构 三个点的解构赋值必须放在所有解构元素的最末尾,否则报错。
let arr = [1, 2, 3, 4]; let [...a] = arr; console.log(a) //[1,2,3,4] 这种做法就是克隆arr数组。
混合解构
混合解构指的是对象和数组混合起来
let obj = { a: { id: 1 }, b: [2, 3] } const { a: {id}, b:[...arr] } = obj; console.log(id, arr) //id = 1, arr = [2, 3]
解构参数
即将参数,尤其是对象数据类型的参数解构为更加易读的代码
function setCookie(name, value, options) { options = options || {}; let secure = options.secure, path = options.path, domain = 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 }); 但是这种情况下,如果不传递参数会报错,所以我们可以将其优化为: function setCookie( name, value, {secure, path, domain, expires} = {} ) { //设置cookie代码 } setCookie("type", "js", { secure: true, expires: 60000 });
六,symbol和symbol属性
原始数据类型
null、undefined ,Number 数字类型,String 字符串,boolean 布尔型
Symbol
Symbol 指的是ES6中的私有属性
Symbol 是一种特殊的、不可变的数据类型,可以作为对象属性的标识符使用。Symbol 对象是一个 symbol primitive data type 的隐式对象包装器。
symbol 数据类型是一个原始数据类型。
创建一个Symbol:
let firstName = Symbol(); let person = {}; person[firstName] = "zxx"; console.log(person[firstName]); //"zxx"
Symbol不能使用new
const name = new Symbol(); //不可以这样做。 //Symbol is not a constructor
最大的用法是用来定义对象的唯一属性名。
使用Symbol:
使用Number的时候,我们可以这样写:
const b = Number(10) // 10 //简写 const b = 10
同理,使用Symbol,我们可以这样:
const name1 = Symbol('sym1'); // Symbol(sym1)
在所有使用可计算属性名的地方,都能使用Symbol类型。比如在对象中的key。
const name = Symbol('name'); const obj = { [name]: "haha" } console.log(obj[name]) // haha
你还可以使用Object.defineProperty()和Object.defineProperties()方法。这2个方法是对象的方法,会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。但是作为Symbol类型key,也不影响使用。
// 设置对象属性只读。3 2 Object.defineProperty(obj, name, {writable: false})
Symbol全局共享
有时候我们可能希望在不同的代码中共享同一个Symbol,在ES6中提供了一个可以随时访问的全局Symbol注册表,它是一个类似于全局作用域的共享环境。如果要创建一个可以共享的Symbol,使用Symbol.for()方法。它只接受要给参数,也就是即将创建的Symbol的字符串的标识符。举例
let uid = Symbol.for("uid"); let object = {}; Object[uid] = "12345"; console.log( Object[uid] ); //"12345" console.log( uid ); // "Symbol(uid)"
Symbol与类型强制转换
JavaScript中的类型可以自动转换。比如Number转换成字符串。
let a = 1; console.log(typeof a); // number console.log(a + ' haha') // '1haha'
但是没有与Symbol逻辑等价的值,因而Symbol使用起来不是很灵活,尤其是不能将Symbol强制转换为字符串和数字类型。
let a = Symbol('a'); console.log(typeof a); console.log(a + ' haha') // Cannot convert a Symbol value to a string
Symbol检索
在ES6中,添加了 Object.getOwnPropertySymbols() 方法,该方法返回一个包含所有Symbol自有属性的数组。
let uid = Symbol.for("uid"); let object = { [uid]: "12345" }; let symbols = Object.getOwnPropertySymbols(object); console.log( symbols.length ); // 1 console.log( symbols[0] ); // "Symbols(uid)" console.log( object[symbols[0]] ); // "12345"
七,set集合与map集合
Set
Set是有序列表,含有相互独立的非重复值。
创建Set new set()
add() :可以向集合中添加元素。
访问集合的 size 属性可以获取集合中目前的元素的数量。
has() 方法,可以判断Set集合中是否存在某个值。
delete() 方法,可以移除Set集合中的某一个元素。
clear() 方法,可以移除集合中的所有元素。
let set = new Set(); set.add(5); set.add("5"); console.log( set.size ); //2 console.log( set.has(5) ); //true console.log( set.has(6) ); //false set.delete(5); console.log( set.has(5) ); //false set.clear(); console.log( set.has("5") ); //false console.log( set.size ); //0
forEach() 方法来迭代 Set 集合。 forEach() 接收的三个参数为:value / key / ownerSet(原来的Set集合)。
set集合中下一次索引的位置 / 与第一个参数一样的值 / 被遍历的set本身
Set本身没有key,而forEach方法中的key被设置成了元素本身。
let set = new Set( [1, 2] ); set.forEach(function(value, key, ownerSet) { console.log( key + " " + value ); console.log( ownerSet === Set ); }); //打印 //1 1 //true //2 2 //true
Set和Array的转换
将数组转为Set集合:给Set构造函数传入数组即可; 将Set转为数组:使用展开运算符。
//数组转换成Set const arr = [1, 2, 2, '3', '3'] let set = new Set(arr); console.log(set) // Set(3) {1, 2, "3"} //Set转换成数组 let set = new Set( [1,2,3,3,3,4,5] ), array = [...set]; console.log( array ); //[1,2,3,4,5]
// 数组去重 let arr = [1, 1, 2, 3]; let unique = [... new Set(arr)]; let a = new Set([1, 2, 3]); let b = new Set([4, 3, 2]); // 并集 let union = [...new Set([...a, ...b])]; // 交集 let intersect = [...new Set([...a].filter(x => b.has(x)))]; // 差集
Weak Set集合
Set集合本身是强引用,只要new Set()实例化的引用存在,就不释放内存,比如你定义了一个DOM元素的Set集合,然后在某个js中引用了该实例,但是当页面关闭或者跳转时,你希望该引用应立即释放内存, Weak Set(弱引用Set集合)中的弱引用如果是对象的唯一引用,则会被回收并释放相应内存。
Weak Set创建、添加、判断、删除方法:
let set = new WeakSet(), key = {}; //向集合中添加对象 set.add(key); console.log( set.has(key) ); // true set.delete(key); console.log(set.has(key)); //false
和Set的区别:
1、 在Weak Set实例中,向add()传非对象参数,会报错;向has()、delete()传非对象参数,则会返回false。
2、Weak Set集合不可以迭代,不能被用于for-of循环,不支持forEach()方法。 3、Weak Set集合不暴露任何迭代器(keys()、values())所以无法通过程序本身来检测其中的内容。 4、Weak Set集合不支持size属性。
Map
Map是存储许多键值对的有序列表,key和value支持所有数据类型。
Map类型是一种存储许多键值对的有序列表。可以通过set()方法向Map集合中添加新元素;通过get()方法从Map集合中获取信息。在对象中,无法用对象作为对象属性的键名,但在Map集合中可以这样做。
Map集合支持的方法。
set()
get()
has(key) //检测指定的键名是否存在 delete(key) //移除指定键名及对应值 clear() //移除Map所有键值对
map.size属性
Map集合的初始化方法
向Map构造函数传入数组来初始化一个Map集合。数组中的每一个元素都是一个子数组,子数组中包含一个键值对的键名与值两个元素
let map = new Map( [["name", "dyx"], ["age", "25"]] ); console.log( map.has("name") ); // true console.log( map.get("name") ); // "dyx" console.log( map.has("age") ); // true console.log( map.get("age") ); // 25 console.log( map.size ); // 2
Map同样可以使用forEach遍历key、value
let map = new Map(); const key = {}; map.set(key, 'a'); map.set('name', 'dyx'); map.set('id', 1); map.forEach((value, key) => { console.log(key, value) }) //Object {} "a" //"name" "dyx" //"id" 1
Weak Map
Weak Map集合中的键名必须是一个对象,如果使用非对象键名会报错。 Weak Map是无序列表。 Weak Map不支持forEach()方法、size属性及clear()方法。
八,迭代器(lterator)和生成器(generator)
迭代器(Iterator)
循环语句的问题
在循环、多重循环中,通过变量来跟踪数组索引的行为容易导致程序出错。迭代器的出现旨在消除这种复杂性,并减少循环中的错误。
什么是迭代器?
迭代器是一种特殊对象。它有一些专门为迭代过程设计的专有接口。例如:next( )方法,该方法用于返回一个结果对象。结果对象有两个属性:value 和 done。value 表示下一个将要返回的值,done 是一个布尔值,当没有更多可返回的数据时,值为 true,否则为 false。如果没有更多数据返回时,value 的值为 undefined。
ES5实现迭代器的代码如下:
//实现一个返回迭代器对象的函数,注意该函数不是迭代器,返回结果才叫做迭代器。 function createIterator(items) { var i = 0; return { next() { var done = (i >= items.length); // 判断i是否小于遍历的对象长度。 var value = !done ? items[i++] : undefined; //如果done为false,设置value为当前遍历的值。 return { done, value } } } } const a = createIterator([1, 2, 3]); //该方法返回的最终是一个对象,包含value、done属性。 console.log(a.next()); //{value: 1, done: false} console.log(a.next()); //{value: 2, done: false} console.log(a.next()); //{value: 3, done: false} console.log(a.next()); //{value: undefined, done: true}
生成器(Generator)
什么是生成器?
-
生成器是一种返回迭代器的函数,通过 function 关键字后的星号 “*” 来表示。生成器函数中也会用到新的关键字 yield。生成器也是ES6的一个重要特性。
-
每当执行完一条 yield 语句后,函数就会自动停止执行。即使是在for循环中使用yield关键字,也会暂停循环
-
使用 yield 关键字可以返回任何值或表达式,所以通过生成器函数批量地给迭代器添加元素。
//生成器函数,ES6内部实现了迭代器功能,使用yield来迭代输出。 function *createIterator() { yield 1; yield 2; yield 3; } const a = createIterator(); console.log(a.next()); //{value: 1, done: false} console.log(a.next()); //{value: 2, done: false} console.log(a.next()); //{value: 3, done: false} console.log(a.next()); //{value: undefined, done: true}
即使是在for循环中使用yield关键字,也会暂停循环 function *createIterator(items) { for(let i = 0; i < items.length; i++) { yield items[i] } } const a = createIterator([1, 2, 3]); console.log(a.next()); //{value: 1, done: false}
yield使用限制
yield只可以在生成器函数内部使用,如果在非生成器函数内部使用,则会报错。
function *createIterator(items) { //你应该在这里使用yield items.map((value, key) => { yield value //语法错误,在map的回调函数里面使用了yield }) } const a = createIterator([1, 2, 3]); console.log(a.next()); //无输出
生成器函数表达式
const createIterator = function *() { yield 1; yield 2; } const a = createIterator(); console.log(a.next());
在对象中添加生成器函数
我们可以在obj中添加一个生成器,也就是添加一个带星号的方法:
const obj = { a: 1, *createIterator() { yield this.a } } const a = obj.createIterator(); console.log(a.next()); //{value: 1, done: false}
可迭代对象和for of循环
迭代器是对象,生成器是返回迭代器的函数。
凡是通过生成器生成的迭代器,都是可以迭代的对象(可迭代对象具有Symbol.iterator属性),也就是可以通过for of将value遍历出来。
function *createIterator() { yield 1; yield 2; yield 3; } const a = createIterator(); for(let value of a) { console.log(value) } // 1 2 3
由于具有 Symbol,iterator 属性的对象都有默认的迭代器,因此可以用它来判断对象是否为可迭代对象。
function isIterable(object) { return typeof object[Symbol.iterator] === "function"; } console.log( isIterable([1,2,3]) ); //true console.log( isIterable(["Hello"]) ); //true console.log( isIterable( new Map() ) ); //true console.log( isIterable( new Set() ) ); //true console.log( isIterable( new WeakMap() ) ); //true console.log( isIterable( new WeakSet() ) ); //true
创建可迭代对象
默认情况下定义的对象(object)是不可迭代的,但是可以通过Symbol.iterator创建迭代器。
const obj = { items: [] } obj.items.push(1);//这样子虽然向数组添加了新元素,但是obj不可迭代 for (let x of obj) { console.log(x) // _iterator[Symbol.iterator] is not a function } //接下来给obj添加一个生成器,使obj成为一个可以迭代的对象。 const obj = { items: [], *[Symbol.iterator]() { for (let item of this.items) { yield item; } } } obj.items.push(1) //现在可以通过for of迭代obj了。 for (let x of obj) { console.log(x) } //1
内建迭代器
在ES6中,有三种类型的集合对象:数组、Map集合、Set集合。这三类集合中都内建了一下三种迭代器:
这三个迭代器,在这三类集合中都可以使用。
1、entries() 返回迭代器:返回键值对
//数组 const arr = ['a', 'b', 'c']; for(let v of arr.entries()) { console.log(v) } // [0, 'a'] [1, 'b'] [2, 'c'] //Set const arr = new Set(['a', 'b', 'c']); for(let v of arr.entries()) { console.log(v) } // ['a', 'a'] ['b', 'b'] ['c', 'c'] //Map const arr = new Map(); arr.set('a', 'a'); arr.set('b', 'b'); for(let v of arr.entries()) { console.log(v) } // ['a', 'a'] ['b', 'b']
2、values() 返回迭代器:返回键值对的value
//数组 const arr = ['a', 'b', 'c']; for(let v of arr.values()) { console.log(v) } //'a' 'b' 'c' //Set const arr = new Set(['a', 'b', 'c']); for(let v of arr.values()) { console.log(v) } // 'a' 'b' 'c' //Map const arr = new Map(); arr.set('a', 'a'); arr.set('b', 'b'); for(let v of arr.values()) { console.log(v) } // 'a' 'b'
3、keys() 返回迭代器:返回键值对的key
//数组 const arr = ['a', 'b', 'c']; for(let v of arr.keys()) { console.log(v) } // 0 1 2 //Set const arr = new Set(['a', 'b', 'c']); for(let v of arr.keys()) { console.log(v) } // 'a' 'b' 'c' //Map const arr = new Map(); arr.set('a', 'a'); arr.set('b', 'b'); for(let v of arr.keys()) { console.log(v) } // 'a' 'b'
不同集合的类型还有自己默认的迭代器,在for of中,数组和Set的默认迭代器是values(),Map的默认迭代器是entries()。
for of循环解构
对象本身不支持迭代,但是我们可以自己添加一个生成器,返回一个key,value的迭代器,然后使用for of循环解构key和value。
const obj = { a: 1, b: 2, *[Symbol.iterator]() { for(let i in obj) { yield [i, obj[i]] } } } for(let [key, value] of obj) { console.log(key, value) } // 'a' 1, 'b' 2
字符串迭代器
ES5中规定可以通过方括号访问字符串中的字符,及 text[0]可以取得字符串 text 的第一个字符,并以此类推。由于方括号操作的是编码单元而非字符,因此无法正确地访问双字节字符。在ES6中,由于全面支持了 Unicode,并且可以通过改变字符串的默认迭代器来使其操作字符,而不是编码单元。此时,可以通过 for-of 循环输出正确的内容。
const str = 'a 吉 a'; for(let v of str) { console.log(v) } // a //空 //吉 //空 //a
NodeList迭代器
自从ES6添加了默认迭代器后,DOM中定义的NodeList类型也拥有了默认迭代器,其行为与数组的默认迭代器完全一致。
const divs = document.getElementByTagName('div'); for(let d of divs) { console.log(d.id) }
展开运算符与非数组可迭代对象
如果想将可迭代对象转换为数组,使用展开运算符是最简单的方法
const a = [1, 2, 3]; const b = [4, 5, 6]; const c = [0,...a, ...b] console.log(c) // [0,1, 2, 3, 4, 5, 6]
高级迭代器功能
1、传参
生成器里面有2个yield,当执行第一个next()的时候,返回value为1,然后给第二个next()传入参数10,传递的参数会替代掉上一个next()的yield返回值。在下面的例子中就是first。
function *createIterator() { let first = yield 1; yield first + 2; } let i = createIterator(); console.log(i.next()); // {value: 1, done: false} console.log(i.next(10)); // {value: 12, done: false}
2、在迭代器中抛出错误
function *createIterator() { let first = yield 1; yield first + 2; } let i = createIterator(); console.log(i.next()); // {value: 1, done: false} console.log(i.throw(new Error('error'))); // error console.log(i.next()); //不再执行
3、生成器返回语句
生成器中添加return表示退出操作。
function *createIterator() { let first = yield 1; return; yield first + 2; } let i = createIterator(); console.log(i.next()); // {value: 1, done: false} console.log(i.next()); // {value: undefined, done: true}
4、委托生成器
生成器嵌套生成器
function *aIterator() { yield 1; } function *bIterator() { yield 2; } function *cIterator() { yield *aIterator() yield *bIterator() } let i = cIterator(); console.log(i.next()); // {value: 1, done: false} console.log(i.next()); // {value: 2, done: false}
异步任务执行器
任务执行器是一个函数,用来循环执行生成器,因为我们知道生成器需要执行N次next()方法,才能运行完,所以我们需要一个自动任务执行器帮我们做这些事情,这就是任务执行器的作用。
给同步函数添加一个延迟方法,即可以将其变为异步函数
function fetchData() { return function(callback) { setTimeout(function() { callback(null, "Hi"); }, 50); } }
九,JavaScript中的类
ES5中的近类结构
ES5及早期版本中没有类的概念,因此用了一个相近的思路来创建一个自定义类型:首先创建一个构造函数,然后定义另一个方法并赋值给构造函数的原型。
function PersonType(name) { this.name = name; } PersonType.prototype.sayName = function() { console.log(this.name); } var person = new PersonType("zxx"); person.sayName(); // "zxx" console.log(person instanceof PersonType); //true console.log(person instanceof Object); //true
ES6 class类
类声明
class PersonClass { //等价于PersonType构造函数 constructor(name) { this.name = name; } //等价于PersonType.prototype.sayName sayName() { console.log(this.name); } } let person = new PersonClass("zxx"); person.sayName(); //"zxx" console.log(person instanceof PersonClass); //true console.log( typeof PersonClass); //"function" console.log( typeof PersonClass.prototype.sayName); //"function"
私有属性:在class中实现私有属性,只需要在构造方法中定义this.xx = xx。自有属性是实例中的属性,不会出现在原型上,只能在类的构造函数或方法中创建,此例中的 name 就是一个自有属性。
类声明和函数声明的区别和特点
1.函数声明可以被提升,而类声明与 let 类似,不能被提升;
2.类声明中的所有代码将运行在严格模式下;
3.在类中,所有方法都是不可枚举的;
4.每个类中,都有一个名为 [[Construct]] 的内部方法,通过关键字new调用那些不含 [[Construct]] 的方法会导致程序抛出错误;
5.使用除 new 以外的方式调用类的构造函数会导致程序抛出错误;
6.在类中,修改类名会导致程序报错。
类表达式
类和函数都有两种存在形式:声明形式和表达形式。声明形式的函数和类都由相应关键字(分别为function,class)进行定义,随后紧跟一个标识符。表达形式的函数和类与之类似,只是不需要再关键字后加标识符
let PersonClass = class { //等价于PersonType构造函数 constructor(name) { this.name = name; } //等价于PersonType.prototype.sayName sayName() { console.log(this.name); } } let person = new PersonClass("zxx"); person.sayName(); //"zxx" console.log(person instanceof PersonClass); //true console.log(person instanceof Object); //true console.log( typeof PersonClass); //"function" console.log( typeof PersonClass.prototype.sayName); //"function"
类是一等公民
一等公民是指一个可以传入函数,可以从函数返回,并且可以赋值给变量的值。
1、可以将类作为参数传入函数。
//新建一个类 let A = class { sayName() { return 'a' } } //该函数返回一个类的实例 function test(classA) { return new classA() } //给test函数传入A let t = test(A) console.log(t.sayName()) // a
访问器属性
尽管应该在类构造函数中创建自己的属性,但类也支持直接在原型上定义访问器属性。创建 getter 时,需要在关键字 get 后紧跟一个空格和相应的标识符;创建 setter 时,只需要把 getter 关键字 get 替换为set即可。
可计算成员名称
类方法和访问器属性也支持使用可计算名称。用方括号包裹一个表达式,即可使用可计算名称。
let methodName = "sayName"; class PersonClass { constructor(name) { this.name = name; } [methodName]() { console.log(this.name); } }; let me = new PersonClass("zxx"); me.sayName(); // "zxx" 同样地,在访问器属性中也可以使用可计算名称。例如: let propertyName = "html"; class CustomHTMLElement { constructor(element) { this.element = element; } get [propertyName]() { return this.element.innerHTML; } set [propertyName]() { this.element.innerHTML = value; } }
生成器方法
在类中,也可以通过 “*” 来定义生成器。如果类是用来表示值的集合的,那么为它定义一个默认迭代器会更有用。
class A { *printId() { yield 1; yield 2; yield 3; } } let a = new A() let x = a.printId() x.next() // {done: false, value: 1} x.next() // {done: false, value: 2} x.next() // {done: false, value: 3}
静态成员
静态成员是指在方法名或属性名前面加上static关键字,和普通方法不一样的是,static修饰的方法不能在实例中访问,只能在类中直接访问。
class PersonClass { //等价于PersonType的构造函数 constructor(name) { this.name = name; } //等价于PerosonType.prototype.sayName sayName() { console.log(this.name); } //等价于PersonType.create() static create(name) { return new PersonClass(name); } } let person = PersonClass.create("zxx"); // PersonClass.create is not a function
继承与派生类
1)使用 extends 关键字可以指定类继承的函数。
2)通过 super() 方法即可访问基类的构造函数。
3)继承自其它类的类被称作派生类。
4)如果不使用构造函数,则当创建新的类实例时会自动调用 super() 并传入所有参数。
在派生类中,如果使用了构造方法,就必须使用super()。
class Component { constructor([a, b] = props) { this.a = a this.b = b } add() { return this.a + this.b } } class T extends Component { constructor(props) { super(props) } } let t = new T([2, 3]) console.log(t.add()) // 5
关于super使用的几点要求:
1、只可以在派生类中使用super。派生类是指继承自其它类的新类。
2、在构造函数中访问this之前要调用super(),负责初始化this。
class T extends Component { constructor(props) { this.name = 1 // 错误,必须先写super() super(props) } }
3、如果不想调用super,可以让类的构造函数返回一个对象。
类方法遮蔽
派生类中的方法总会覆盖基类中的同名方法。如果仍然想使用基类中的方法,则可以借助 super 关键字来实现。
class Component { constructor([a, b] = props) { this.a = a this.b = b } //父类的add方法,求和 add() { return this.a + this.b } } class T extends Component { constructor(props) { super(props) } //重写add方法,求积 add() { return this.a * this.b } } let t = new T([2, 3]) console.log(t.add()) // 6
静态成员继承
派生类继承基类后,基类中的静态成员也可以直接在派生类中使用。
class Component { constructor([a, b] = props) { this.a = a this.b = b } static printSum([a, b] = props) { return a + b } } class T extends Component { constructor(props) { super(props) } } console.log(T.printSum([2, 3])) // 5
派生自表达式的类
只要表达式可以被解析为一个函数,并且具有[[Construct]]属性和原型,那么就可以用extends进行派生。
extends强大的功能使得类可以继承自任意类型的表达式,而且可以是不止一个的表达式,这样可以动态地确定类的继承目标,创建不同的继承方法。举例:
function Rectangle(length, width) { this.length = length; this.width = width; } Rectangle.prototype.getArea = function() { return this.length * this.width; } function getBase() { return Rectangle; } class Square extends getBase() { constructor(length) { super(length * length); } } var x = new Square(3); console.log(x.getArea()); //9 console.log(x instanceof Rectangle); //true
内建对象的继承
在ES5中,无法通过继承的方式创建属于自己的特殊数组。而ES6类语法的一个目标是支持内建对象的继承。具体是:先由基类(Array)创建this的值,然后派生类的构造函数(MyArray)再修改这个值。举例:
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可以定义当派生类的方法返回实例时,应该返回的值的类型。
在构造函数中使用new.target
类的构造函数必须通过new关键字调用,所以总是在类的构造函数中定义new.target属性。但是其值有时会不同。每个构造函数都可以根据自身被调用的方式改变自己的行为。例如,可以用new.target创建一个抽象基类(不能被直接实例化的类)
//抽象基类 class Shape { constructor() { if (new.target === Shape) { throw new Error("这个类不能被直接实例化"); } } } class Rectangle extends Shape { constructor(length, width) { super(); this.length = length; this.width = width; } } var x = new Shape(); // 抛出错误 var y = new Rectangle(3,4); //没有错误 console.log(y instanceof Shape); // true
十,改进的数组功能
创建数组
在ES6以前,创建数组的方式有两种:一种是调用Array构造函数,一种是使用数组自面量语法。由于在使用Array构造函数创建数组的时候,有时会有产生一定的怪异行为(如传入一个字符串数字的时候)。所以增加了 Array.of( ) 和 Array.from( ) 方法。
Array.of()
ES5中new一个人数组的时候,会存在一个令人困惑的情况。当new一个数字的时候,生成的是一个长度为该数字的数组,当new一个字符串的时候,生成的是该字符串为元素的数组。
const a = new Array(2) const b = new Array("2") console.log(a, b) //[undefined, undefined] ["2"]
这样一来,导致new Array的行为是不可预测的,Array.of()出现为的就是解决这个情况。
const c = Array.of(2) const d = Array.of("2") console.log(c, d) // [2] ["2"]
使用Array.of()创建的数组传入的参数都是作为数组的元素,而不在是数组长度,这样就避免了使用上的歧义。
Array.from()
Array.from( ) 常用来将类数组对象(如arguments)、可迭代对象转化为数组。它接受可迭代对象或者类数组对象作为第一个参数,最终返回一个数组。
下面的例子讲的是将arguments转换成数组。arguments是类数组对象,他表示的是当前函数的所有参数,如果函数没有参数,那么arguments就为空。
function test(a, b) { let arr = Array.from(arguments) console.log(arr) } test(1, 2) //[1, 2]
映射转换:Array.from(arg1, arg2),我们可以给该方法提供2个参数,第二个参数作为第一个参数的转换。
function test(a, b) { let arr = Array.from(arguments, value => value + 2) console.log(arr) } test(1, 2) //[3, 4]
Array.from还可以设置第三个参数,指定this。
Array.from()转换可迭代对象:这个用法只需要一个例子,数组去重。
function test() { return Array.from(new Set(...arguments)) } const s = test([1, "2", 3, 3, "2"]) console.log(s) // [1,"2",3]
给数组添加新方法
ES6给数组添加了几个新方法:find()、findIndex()、fill()、copyWithin()。
1、find():传入一个指定的参数,找到数组中符合当前搜索规则的第一个元素,返回它,并且终止搜索。
const arr = [1, "2", 3, 3, "2"] console.log(arr.find(n => typeof n === "number")) // 1
2、findIndex():传入一个指定的参数,找到数组中符合当前搜索规则的第一个元素,返回它的下标,终止搜索。
const arr = [1, "2", 3, 3, "2"] console.log(arr.findIndex(n => typeof n === "number")) // 0
3、fill():fill( )方法可以用指定的值填充一至多个元素。当传入一个值时,fill( )方法会用这个值重写数组中的所有值。如果只想改变某一个或者某一部分的值,可以传入开始索引和不包含结束索引(不包含结束索引当前值)这两个参数。
let numbers = [1,2,3,4]; numbers.fill(1); console.log(numbers.toString()); // 1,1,1,1 let numbers = [1,2,3,4]; numbers.fill(1,2); console.log(numbers.toString()); //1,2,1,1 numbers.fill(0,1,3); console.log(numbers.toString()); // 1,0,0,1
4、copyWithin(): copyWith( )方法与 fill( )方法类似,不过它是从数组中复制元素的值。调用该方法需要传入两个参数:一个是该方法开始填充值的位置,一个是该方法复制值的索引位置。如果给该方法中传入了第3个参数,则表示停止复制值的位置(但是该参数是不包含结束索引)
let numbers = [1,2,3,4]; numbers.copyWith(2,0); console.log(numbers.toString()); //1,2,1,2
定型数组
所谓定型数组,是指将任何数字转换为一个包含数字比特的数组,是一种用于处理数值类型数据的专用数组,可以为JavaScript提供快速的按位运算。
定型数组提供了适用面更广的API和更高的性能。设计定型数组的目的就是提高与WebGL等原生库交换二进制数据的效率。由于定型数组的二进制表示对操作系统而言是一种容易使用的格式,JavaScript引擎可以重度优化算术运算、按位运算和其他对定型数组的常见操作,因此使用它们速度极快。
定型数组支持存储、操作以下8种不同的数值类型:
有符号的8位整数 (int8)
无符号的8位整数 (uint8)
有符号的16位整数 (int16)
无符号的16位整数 (uint16)
有符号的32位整数 (int32)
无符号的32位整数 (uint32)
32位浮点数 (float32)
64位浮点数 (float64)
数组缓冲区:是一段可以包含特定数量字节的内存地址。可以通过ArrayBuffer构造函数来创建数组缓冲区。//let buffer = new ArrayBuffer(10); //分配10字节
定型数组与普通数组的相似之处
-
通用方法
数组中大部分的方法都可以在定型数组中使用。
-
相同的迭代器
展开运算符能够将可迭代对象转换为普通数组,也可以将定型数组转换为普通数组。
-
of( )、from( )方法
定型数组也可以像普通数组那样使用of( ) 和 from( ) 方法,只不过返回的也是定型数组。
定型数组和普通数组的差别
-
行为差异
当操作普通数组的时候,可以改变数组的大小,但是定型数组始终保持相同的尺寸。
-
缺失的方法
这些方法只在普通数组中有,而在定型数组中没有。想一想是为什么呢?因为这几个方法基本上都是以改变数组大小为目的的,而我们刚才说的第一条里面,定型数组始终是保持相同尺寸的,所以呢,定型数组中也就不包含这些方法了。列举一下这些方法:
concat( )
pop( )
push( )
shift( )
unshift( )
splice( )
上面的方法中,除了concat( )之外,其它几个都可以改变数组的尺寸大小。
十一,promise与异步编程
是异步编程的一种解决方案。Promise 是一个对象,从它可以获取异步操作的消息。,它既可以像事件和回调函数一样指定稍后执行的代码,也可以明确指示代码是否成功执行。
异步编程的背景知识
机制
JavaScript引擎是基于单线程(Single-threaded)事件循环的概念构建,即同一时刻只允许一个代码块在执行。这些代码块被放在一个任务队列(job queue)中,每当一段代码准备执行时,都会被添加到任务队列。每当JavaScript引擎中的一段代码结束执行,事件循环(event loop)会执行队列中下一个任务。事件循环是JavaScript引擎的一段代码,负责监控代码执行并管理任务队列。
事件模型
例如点击按钮或者按下键盘按键会触发的onclick事件,是JavaScript中最基础的异步编程形式。尽管事件模型适用于响应用户交互和完成类似的低频功能,但对更复杂的需求来说却不是很灵活。
回调模式
回调模式和事件模型类似,异步代码都会在未来的某个时间点执行,二者的区别是回调模式中被调用的函数是作为参数传入的。
readFile("example.txt", function(err, contents) { if (err) { throw err; } console.log(contents); }); console.log("Hi");
Promise
Promise相当于异步操作结果的占位符,它不去订阅事件,也不会传递一个回调函数给目标参数,而是让函数返回一个Promise
Promise的声明周期
var myFirstPromise = new Promise(function(resolve, reject){ //当异步代码执行成功时,我们才会调用resolve(...), 当异步代码失败时就会调用reject(...) //在本例中,我们使用setTimeout(...)来模拟异步代码,实际编码时可能是XHR请求或是HTML5的一些API方法. setTimeout(function(){ resolve("成功!"); //代码正常执行! }, 250); }); myFirstPromise.then(function(successMessage){ //successMessage的值是上面调用resolve(...)方法传入的值. //successMessage参数不一定非要是字符串类型,这里只是举个例子 document.write("Yay! " + successMessage); });
Promise先是处于进行中(pending)的状态,此时操作尚未完成;等异步操作执行结束后,Promise变为已处理(settled)状态。分为两种状态,已经完成(fulfilled),拒绝(rejected)
Promise的状态改变时,调用then()方法来采取特定的行动。
then()方法接受2个参数:第一个参数是变为fulfilled时要调用的函数,第2个是状态变为rejected时需要调用的函数。
Promise还有一个catch()方法,相当于只给其传入拒绝处理程序的then()方法
创建未完成的Promise
用Promise构造函数可以创建新的Promise,构造函数只接受一个参数:包含初始化Promise代码的执行器(executor)函数。执行器接受2个参数,分别是执行成功完成时调用的resolve()函数,执行失败时,调用reject()函数。执行器函数会立即执行,然后才执行后续流程中的代码(即resolve() / reject()会放到任务队列中再执行)。
创建已处理的Promise
使用Promise.resolve() / Promise.reject()来实现根据特定的值来创建已解决的Promise。
let promise = Promise.resolve(42); promise.then(function(value) { console.log(value); //42 }); let promise = Promise.reject(42); promise.catch(function(value){ console.log(value); // 42 });
执行器错误
每个执行器中都隐含一个try-catch块,所以错误会被捕获并传入拒绝处理程序。
使用Promise构建函数创建新的Promise
Promise构造函数只有一个参数,该参数是一个函数,被称作执行器,执行器有2个参数,分别是resolve()和reject(),一个表示成功的回调,一个表示失败的回调。
new Promise(function(resolve, reject) { setTimeout(() => resolve(5), 0) }).then(v => console.log(v)) // 5
记住,Promise实例只能通过resolve或者reject函数来返回,并且使用then()或者catch()获取,不能在new Promise里面直接return,这样是获取不到Promise返回值的。
1、我们也可以使用Promise直接resolve(value)。
Promise.resolve(5).then(v => console.log(v)) // 5
2、也可以使用reject(value)
Promise.reject(5).catch(v => console.log(v)) // 5
3、执行器错误通过catch捕捉。
new Promise(function(resolve, reject) { if(true) { throw new Error('error!!') } }).catch(v => console.log(v.message)) // error!!
Promise的其他方法
在Promise的构造函数中,除了reject()和resolve()之外,还有2个方法,Promise.all()、Promise.race()。
Promise.all():
该方法只接受一个参数并返回一个Promise,该参数是一个含有多个受监视Promise的可迭代对象,只有当可迭代对象中所有Promise都被解决后,返回的Promise才会被解决,只有当可迭代对象中所有Promise都被完成后返回的Promise才会被完成。
语法很简单:参数只有一个,可迭代对象,可以是数组,或者Symbol类型等。
Promise.all(iterable).then().catch()
当迭代对象中所有的Promise都被解决并返回后,最后的Promise里面存的值按照传入参数数组中的Promise的顺序储存
let p1 = new Promise(function(reolve, reject) { resolve(42); }); let p2 = new Promise(function(resolve, reject) { resolve(43); }); let p3 = new Promise(function(resolve, reject) { resolve(44); }); let p4 = Promise.all([p1, p2, p3]); p4.then(function(value) { console.log( Array.isArray(value) ); // true console.log( value[0] ); // 42 console.log( value[1] ); // 43 console.log( value[2] ); // 44 });
2.当迭代对象中有被拒绝的Promise时,只要有一个被拒绝,那么返回的Promise没等所有Promise都完成就立即被拒绝,
let p1 = new Promise(function(reolve, reject) { resolve(42); }); let p2 = new Promise(function(resolve, reject) { reject(43); }); let p3 = new Promise(function(resolve, reject) { resolve(44); }); let p4 = Promise.all([p1, p2, p3]); p4.then(function(value) { console.log( Array.isArray(value) ); // true console.log( value ); // 43 });
Promise.race(): Promise.race()与Promise.all()稍有不同。在可迭代对象中,只要有一个Promise被解决,返回的Promise就解决,无须等到所有Promise都被完成。
如果先解决的是已完成Promise,则返回已完成Promise;如果先解决的是已拒绝的Promise,则返回已拒绝Promise。
let p1 = new Promise(function(resolve, reject) { setTimeout(function() {resolve(42);}, 0); }); let p2 = Promise.reject(43); let p3 = new Promise(function(resolve,reject) { resolve(44) }); let p4 = Promise.race([p1, p2, p3]); p4.catch(function(value) { console.log(value); // 43 });
Promise和异步的联系
只要每个异步操作都返回Promise,以Promise作为通用接口用于所有异步代码可以简化任务执行器。
Promise本身不是异步的,只有他的then()或者catch()方法才是异步,也可以说Promise的返回值是异步的。通常Promise被使用在node,或者是前端的ajax请求、前端DOM渲染顺序等地方。
十二,代理(proxy)与反射(reflection)
代理和反射
-
调用new Proxy()可以创建代理。代理可以拦截 JavaScript 引擎内部目标的底层对象操作,这些底层操作被拦截后会触发响应特定操作的陷阱函数。
-
反射以 Reflect 对象的形式出现,每个代理陷阱对应一个命名和参数都相同的 Reflect 方法。
创建一个简单的代理
用 Proxy 构造函数创建代理需要传入两个参数:目标(target)和处理程序(handler)。不使用任何陷阱的处理程序等价于简单的转发代理。
let target = {}; let proxy = new Proxy(target, {}); proxy.name = "proxy"; console.log(proxy.name); // "proxy" console.log(target.name); // "proxy" target.name = "target"; console.log(proxy.name); //"target" console.log(target.name); //"target"
反射 Reflect
反射的概念
Reflect 是一个内置的对象,它提供可拦截JavaScript操作的方法。方法与代理处理程序的方法相同。Reflect 不是一个函数对象,因此它是不可构造的。
反射的使用
Reflect提供了一些静态方法,静态方法是指只能通过对象自身访问的的方法
静态方法列表:这么多静态方法,你需要学会的是如何使用它们。
1、Reflect.apply() 2、Reflect.construct() 3、Reflect.defineProperty() 4、Reflect.deleteProperty() 5、Reflect.enumerate() 6、Reflect.get() 7、Reflect.getOwnPropertyDescriptor() 8、Reflect.getPrototypeOf() 9、Reflect.has() 10、Reflect.isExtensible() 11、Reflect.ownKeys() 12、Reflect.preventExtensions() 13、Reflect.set() 14、Reflect.setPrototypeOf()
静态方法的使用:
demo1:使用Reflect.get()获取目标对象指定key的value。
let obj = { a: 1 }; let s1 = Reflect.get(obj, "a") console.log(s1) // 1
demo2:使用Reflect.apply给目标函数floor传入指定的参数。
const s2 = Reflect.apply(Math.floor, undefined, [1.75]); console.log(s2) // 1
进一步理解Reflect
Reflect可以拦截JavaScript代码,包括拦截对象,拦截函数等,然后对拦截到的对象或者函数进行读写等操作。
比如demo1的get()方法,拦截obj对象,然后读取key为a的值。当然,不用反射也可以读取a的值。
再看demo2的apply()方法,这个方法你应该比较了解了,和数组中使用apply不同的是,Reflect.apply()提供了3个参数,第一个参数是反射的函数,后面2个参数才是和数组的apply一致。demo2的例子我们可以理解成是拦截了Math.floor方法,然后传入参数,将返回值赋值给s2,这样我们就能在需要读取这个返回值的时候调用s2。
//数组使用apply const arr = [1, 2, 3] function a() { return Array.concat.apply(null, arguments) } const s = a(arr) console.log(s) // [1, 2 ,3]
代理 Proxy
let p = new Proxy(target, handler);
target:一个目标对象(可以是任何类型的对象,包括本机数组,函数,甚至另一个代理)用Proxy来包装。 handler:一个对象,其属性是当执行一个操作时定义代理的行为的函数。
代理的使用
主要看new Proxy({}, handler)的操作,指定目标obj对象,然后handler对象执行get()操作,get()返回值的判断是,如果name是target目标对象的属性,则返回target[name]的值,否则返回37,最后测试的时候,p.a是对象p的key,所以返回a的value,而p.b不存在,返回37。
const obj = { a: 10 } let handler = { get: function(target, name){ console.log('test: ', target, name) // test: {"a":10} a // test: {"a":10} b return name in target ? target[name] : 37 } } let p = new Proxy(obj, handler) console.log(p.a, p.b) // 10 37
这个例子的作用是拦截目标对象obj,当执行obj的读写操作时,进入handler函数进行判断,如果读取的key不存在,则返回默认值。
解决跨域访问
module.exports = { devServer: { proxy: [ { context: "/api/*", //代理API target: 'https://www.hyy.com', //目标URL secure: false } ] } }
十三,用模块封装代码
模块的定义
模块是自动运行在严格模式下,并且没有办法退出运行的JavaScript代码。
模块可以是函数、数据、类,需要指定导出的模块名,才能被其他模块访问。
//数据模块 const obj = {a: 1} //函数模块 const sum = (a, b) => { return a + b } //类模块 class My extends React.Components { }
模块的特性
a) 在模块顶部创建的变量,仅在模块的顶级作用域中存在,不会自动被添加到全局共享作用域。
b) 模块必须导出一些外部可以访问的元素,如变量、函数。
c) 在模块的顶部,this 值是 undefined。
d) 模块不支持 HTML 风格的代码注释。
模块的导出
给数据、函数、类添加一个export,就能导出模块。一个配置型的JavaScript文件中,你可能会封装多种函数,然后给每个函数加上一个export关键字,就能在其他文件访问到。
//数据模块 export const obj = {a: 1} //函数模块 export const sum = (a, b) => { return a + b }
模块的引用
在另外的js文件中,我们可以引用上面定义的模块。使用import关键字,导入分2种情况,一种是导入指定的模块,另外一种是导入全部模块。
1、导入指定的模块。
//导入obj数据,My类 import {obj, My} from './xx.js' //使用 console.log(obj, My)
2、导入全部模块
//导入全部模块 import * as all from './xx.js' //使用 console.log(all.obj, all.sun(1, 2), all.My)
默认模块的使用
如果给我们的模块加上default关键字,那么该js文件默认只导出该模块,你还需要把大括号去掉。
//默认模块的定义 function sum(a, b) { return a + b } export default sum //导入默认模块 import sum from './xx.js'
模块的使用限制
不能在语句和函数之内使用export关键字,只能在模块顶部使用,
在react中,模块顶部导入其他模块。
import react from 'react'
在vue中,模块顶部导入其他模块。
<script> import sum from './xx.js' </script>
修改模块导入和导出名
有2种修改方式,一种是模块导出时修改,一种是导入模块时修改。
1、导出时修改:
function sum(a, b) { return a + b } export {sum as add} import { add } from './xx.js' add(1, 2)
2、导入时修改:
function sum(a, b) { return a + b } export sum import { sum as add } from './xx.js' add(1, 2)
无绑定导入
当你的模块没有可导出模块,全都是定义的全局变量的时候,你可以使用无绑定导入。
模块:
let a = 1 const PI = 3.1314
无绑定导入:
import './xx.js' console.log(a, PI)
浏览器加载模块
Web浏览器中的模块加载顺序:
其中,模块按照它们出现在HTML文件中的顺序执行。
<!-- 先执行这个标签 --> <script type="module" src="module1.js"></script> <!-- 再执行这个标签 --> <script type="module" src="module2.js"> import {sum} from "./example.js" let result = sum(1,2); </script> <!-- 最后执行这个标签 --> <script type="module" src="module2.js"></script>
浏览器模块说明符解析
模块说明符(module specifier)在前面的例子都是相对路径(如:字符串“./example.js”)。浏览器要求模块说明符具有以下几种格式之一:
以 / 开头的解析为从根目录开始。
以 ./ 开头的解析为从当前目录开始。
以 ../ 开头的解析为从父目录开始。