目录
- 1. ES6版本过度历史
- 2. 预编译回顾、变量污染解决办法、kiss原则、let初识、暂时性死区
- 3. let进阶、const、全部变量与顶层对象
- 4. 函数默认值、数组解构、对象解构
- 5. 解构本质、()用法、解构其他用法、函数参数解构、解构隐式转换、解构书写规范
- 6. 函数默认值特殊情况、this指向回顾、箭头函数、rest运算符
- 7. 箭头函数的实质、箭头函数的使用场景
- 8. 获取函数名、对象拓展、静默失败、描述符、getter/setter
- 9. 查看getter/setter函数名、对象密封4种方式、is方法、assign合并与覆盖、assign拷贝、无法拷贝getter/setter的问题、浅拷贝、浅拷贝新方式、部署对象的方式
- 10. 更改原型、key/value/entries遍历、super、symbol遍历、for和keyFor、几种遍历方式对比
- 11. 箭头函数/rest运算符拓展、Symbol、迭代器初识、数据结构、迭代器实现、typeArray、forOf
- 12. 数组构造器新增方法(of、from)、数组原型新增方法(fill、keys/values/entries、copyWithin、find/findIndex、includes)、数值拓展(进制的表示、数值方法调整、数值新增方法 )、Math拓展
- 13. 正则新增特性(声明方式、方法调整、修饰符yus、新增方法、UTF_16编码方式)
- 14. 字符串拓展(字符表示、码点相关方法(length、charAt、charCodeAt 、codePointAt、forOf、fromCharCode、fromCodePoint)、字符串新增方法(includes / startsWith / endsWith、repeat、padStart / padEnd))、模板字符串(模板渲染、注入问题、标签模板)
- 15. map与set、set对比map、set/map对比array/object
- 16. WeakMap与WeakSet、proxy与reflect
- 17. class类、extends继承、super、class源码、修饰器模式
- 18. 异步的开端(JS执行机制、异步存在的问题、promise演变历史)、promise初识(三种状态(padding/resolve/reject)、executor执行者、then回调绑定、执行顺序、链式调用)
- 19. promise深入(then简写、catch方法、promise固化、状态依赖、异步管理方法(all/race)、thenable、构造器上的resolve/reject、函数Promise化)、promisify函数封装
- 20. 迭代器深入(迭代器的实现、迭代器与迭代器模式、内部迭代器和外部迭代器、部署迭代器接口、默认调用迭代器的情况、)、generator(function*fn{}、yield、状态机 *、co)
- 21. async与await、all错误忽略实现
- 22. Promise源码重写
- 23. JavaScript模块化
- 24. 生成器与迭代器的应用
1. ES6版本过度历史
1.1 历史
1995 liveScript(后改名为JavaScript1996年改名)
1996 javascript 1.0 1.1;
1997 Jscript
1997.6 ECMAScript 1.0
1998.6 ECMAScript 2.0
1999.12 ECMAScript 3.0
2000 ECMAScript 4.0 草案没有通过 TC39
2007 ECMAScript 4.0 准备发布,不发布
2008.7 ECMAScript 3.1(改名为ECMA5)大会项目代号(hamony)
2009.12 ECMAScript5(正式发布)/ JavaScript.next(放入草案)/ JavaScript.next.next(放入草案)
2011.6 ECMAScript5.1
2013.3 javaScript.next(草案冻结)
2013.6 javaScript.next(草案发布)
2015.6 ECMA6正式发布(ECMAScript2016/7/8都是指ES6更迭的版本)
1.2 总结
es4是因为它的语法太激进所以被分为了两个草案,即es.next,es.next.next
es5发布于2009年,其实用的是99年发布的ecma3.0的改进3.1,es6发布于15年
es6(es.next)实际上在2000就已经提出来了,时隔了15年时间才发布
2. 预编译回顾、变量污染解决办法、kiss原则、let初识、暂时性死区
2.1 预编译回顾
-
AO预编译过程:
- 创建AO对象
- 找变量声明, 形参赋值undefined
- 形参和实参相统一(包括赋值函数默认值)
- 找function赋值为函数体
- 执行
-
GO预编译过程:
- 创建GO对象,
- var关键字赋值undefined
- 找function赋值为函数体
- 执行
2.2 变量污染解决方法
- 用立即执行函数创造一个独立的作用域,但函数内部也会造成变量污染
- 用let、const声明变量
2.3 KISS原则
kisskeep it simple stupid 即简单又愚蠢
其核心思想就是“解耦,解耦,再解耦”,让函数职责单一化,功能单一化去解决一个问题
2.4 let初识
-
概念
let与之相关联的一个概念叫做块级作用域
什么是块?
就是一个大括号 {}。比 如if语句、for语句或单独一个大括号都属于块
-
特征
-
在同一作用域下不能重复声明
//错误示例 let a = 1; let a = 2; //报错 function test() { let a = 1; let a = 2; //报错 } test(); function test() { let a = 1; var a = 2; //报错 } test(); function test(a) { let a = 10; //报错 console.log(a); } test(); function test(a){ // 不是同一作用域,所以不报错 { let a =10; console.log(a); //10 } console.log(a);//undefined } test();
-
let不会声明提升,会产生一个暂时性死区
//报错示例 console.log(a); //报错 let a = 10; var a = a; console.log(a); //undefined let b = b; //报错,b还没有赋值执行就会报错 console.log(b); function test(x = y, y = 2){ // 报错,y没有声明 解决方案:x = 2, y = x console.log(x, y); } test(); //证明typeof不再安全 console.log(typeof(a)) //报错,不是typeof引起的错误,而是暂时性死区引起的错误 let a
-
let只能在当前的作用域下生效
//错误示例 { let a =2; } console.log(a); //报错 //注意这是个坑 for(;1;){ let a = 1; } console.log(a);//这样不报错,因为就一直在循环,是个坑 //第二坑 for(let i = 0; i < 10; i++){ } console.log(i); //报错,let声明在括号里面依然属于块级作用域的范畴 //第三坑 var arr = []; for(var i = 0; i<10;i++){ arr[i] = function(){ //注意赋值的是整个函数体并没有执行,里面的i只是一个变量没有值,只有执行它的时候才会去获取值,那时候去获取i就是10,因为for循环完了以后i就等于10 console.log(i); } } for(var i = 0; i<10;i++){//这里i二次声明,把上一次的i直接覆盖掉,所以打印出来的是0-9,而不是10个10 console.log(arr[i]()); } //第四坑 for(let i = 0; i < 10; i++){ var i = 'a'; //报错,因为i声明提升,而let不允许重复声明,所以报错 console.log(i); } for(let i = 0; i < 10; i++){ let i = 'a'; console.log(i); //10个a } // (){}在同一个作用域,{}是()的子作用域
-
2.5 暂时性死区
在代码块内,使用let和const命令声明变量之前或者当前,该变量都是不可用的,语法上被称为暂时性死区
当前是指什么?比如:let x = x,x正在被声明,所以无法使用,也是一种死区
2.6 总结
let本质上就是为了js增加一个块级作用域,let所在的地方就是块级作用域所在的地方(前提是不同作用域)
es5只能在顶级作用域和函数作用域下声明函数,块级作用域下会报错(es6可以在块级作用域下声明函数,建议用函数表达式声明)
块级作用域没有返回值
块级作用域不等于匿名函数的立即调用(可以模拟但不等于,因为块级作用域没有返回值)
3. let进阶、const、全部变量与顶层对象
3.1 let进阶
-
闭包问题
var arr = []; for(let i = 0; i< 10 i++){ arr[i] = function(){ //赋值10个函数体的同时,也生成了10个闭包 console.log(i) } } for(var k = 0; k <10; k++){ arr[k](); } //输出0-9 //通过函数形式形成作用域 var arr = []; var _loop = function _loop(i) { //每次循环i拿的是闭包参数的i,和全局没有关系,全局的i是10 arr[i] = function() { //这里产生了闭包,函数声明的时候捆绑着周围的环境,缓存到函数作用域链 console.log(i); }; }; for (var i = 0; i < 10; i++) { _loop[i](); }; for (var i = 0; i < 10; i++) { arr[i](); }; { let i = 0; // 这里生成了块级作用域,效果和函数作用域是一样的 { arr[i] = function(){ //这里也产生了闭包,每次循环i拿的是外层作用域的i console.log(i); }; }; };
-
块级作用域下声明函数
{ let a = 0; function a(){}; //报错,重复声明了 } { let a = 1; { function a() { } //这里函数声明提升只能提升到当前作用域,不能提升到作用域之外,所以不会报错(函数表达式提升到全局) } console.log(a); //1 }
3.2 const
-
概念
用来定义常量,即不可变的量,与let一样会产生一个块级作用域
-
特征
-
一旦定义必须赋值,且值不能被更改
const a; console.log(a); //报错 const a = 12; a=10; //报错
特殊情况:
const obj = {} obj.name = 'zhangsan'; //{name: 'zhangsan'} //const定义的常量只能保证栈的地址不变,无法保证引用里数据 结构的改变
解决方法:freeze-冻结
const obj = []; Object.freeze(obj); obj[2] = 'zhangsan';//无效 //循环冻结 function myFreeze(obj){ Object.freeze(obj); for(var key in obj){ if(typeof(obj[key]==='object')&& obj[key]!==null){ //排除null的情况 Object.freeze(obj[key]); } } } // 但这种方式并不好,因为它没有从源头上解决问题
const http = require('http') // 从源头解决:require返回的是实例化的对象,无论怎么修改http,都不影响构造器,所以不用冻结
-
有块级作用域,不会变量提升,存在暂时性死区
{ const a = 12; //自带块级作用域 } console.log(a); //报错 { console.log(a); //报错 const a = 12; }
-
与let一样不能重复声明
const a =12; //报错 var a =10;
-
3.3 全局变量与顶层对象
顶层对象的属性和全局变量是等效的,这样导致了全局变量会挂到window的问题
ES6为了改变了这种现状,为了兼容允许 function,var 声明全局变量,不允许let,const声明全局变量
在不同的环境下面顶层对象是不一致的,例如在浏览器环境中顶层对象就是window,node环境中的顶层对象就是global
ES6提案让所有的环境都有一个顶层对象-global,只提案没有实施
4. 函数默认值、数组解构、对象解构
4.1 函数默认值
-
暂时性死区
暂时性死区 TDZ(Temporal Dead Zone)
var x = 1; { let x = x; //报错 log(x) }
这里的x取的是let产生的块级作用域中的x,由于暂时性死区问题,x还没有声明,所以报错
侧面印证了let声明的变量,访问不到父级作用域
-
参数默认值
-
ES3写法
function foo(x,y){ x = x || 1; y = y || 2; console.log(x+y); } foo(); //3 foo(5,6); //11 foo(5); //7 foo(null,6);//7 foo(0,5);//6 这里就会出问题这是falsy(虚值Boolean为假的值),我赋值的是0
-
ES5写法
//es5解决方案 function foo(x, y) { var a = typeof (arguments[0]) !== 'undefined' ? arguments[0] : 1; var b = typeof (arguments[1]) !== 'undefined' ? arguments[1] : 2; console.log(a + b); } foo(); //3 foo(5, 6); //11 foo(5); //7 foo(null, 6);//6 null转换成了0 foo(0, 5);//5
虚值:在通过boolean转化的时候可以转化为假的值就是falsy(虚值)
-
ES6写法
function foo(x = 1, y = 2) { console.log(x + y); } foo(); //3 foo(5, 6); //11 foo(5); //7 foo(null, 6); //6 foo(0, 5) //5
-
作用域深入
let x =1; function foo(y = x){ let x =2; console.log(y); } foo();//输出1,这里可以打印出1的原因是let声明的是y产生了块级作用域,所有y访问不到父级作用域的变量,但x可以拿到 let x =1; function foo(x = 2){ // ()里的x是let声明的,且与{}的作用域是同级的,x重复声明所以报错 let x = 2; console.log(x); } foo();//报错 let x = 1; function foo(x = x){ // ()里的x是let声明的,且与{}的作用域是同级的,x重复声明所以报错 let x = 2; console.log(x); } foo(); //报错 let x = 1; function foo(x = x) { // 暂时性死区(包括let之前与当前),x还未声明 console.log(x); } foo(); //报错 let x = 1; function foo(x = x) { // 传了实参,默认参数就不会再赋值,所以不报错 console.log(x); } foo(1); //输出1
-
默认值运算
var w = 1, z = 2; function foo(x = w + 1, y = x + 1, z = z + 1) { // 暂时性死区,z还未声明 console.log(x, y, z) } foo(); // 报错 var w = 1, z = 2; function foo(x = w + 1, y = x + 1) { console.log(x, y) } foo(); // 2、3
-
惰性求值
每一次都要重新计算表达式的值
// 函数的默认参数为表达式的情况下的加载方式是一个惰性求值的方式,即不会缓存默认值,每次调用都会重新计算 let a = 99; function foo(b = a + 1){ console.log(b); } foo(); a = 100; foo();//输出100,101 惰性求值
-
4.2 解构赋值-数组
-
模式匹配(结构化复制)
两边的结构要一致
let [a, b, c] = [1, 2, 3] console.log(a, b, c) // 1 2 3 let [d, [e], [f]] = [1, [2], [3]] console.log(d, e, f) // 1 2 3
-
解构失败
变量多了,值少了
let [a, [b,c], [d,e]] = [1, [2,3], [,]] console.log(a,b,c,d,e) // 1 2 3 undefined undefined
-
不完全解构
值多了,变量少了
let [a, [,], [d,e]] = [1, [2,3], [4,5]] console.log(a,b,c,d,e) // 1 4 5
-
解构默认值
let [a = 6] = [1]; console.log(a); // 1 let [a = 6] = []; console.log(a); // 2 let [a, b = 2] = [1]; console.log(a, b); // 1 2 let [a, b = 2] = [1, undefined]; // 除了undefined取默认值以外取的所有值都是原本的值,JS引擎会认为是空 console.log(a, b); // 1 2 let [a, b = 2] = [1, null]; console.log(a, b); // 1 null function test() { console.log(10); } let [x = test()] = [1]; console.log(x); // 1 function test() { // 函数默认返回undefined console.log(10); } let [x = test()] = []; console.log(x); // undefined let [x = 1, y = x] = []; console.log(x, y) // 1 1 let x = 5; let [x = 1, y = x] = []; // 这个不存在块级作用域,这个x已经被定义,重复声明报错 console.log(x, y) // 报错 let [x = 1, y = x] = [2]; console.log(x, y) // 2 2 let [x = 1, y = x] = [1,2] console.log(x,y) // 1 2 let [x = y, y = 1] = []; // 暂时性死区,y还未声明 console.log(x, y); // 报错
4.3 解构赋值-对象
-
属性和方法的简写
var _name = 'zhangsan'; var age = 18 var person = { _name, age, sex: 'male', eat() {console.log(1)} } console.log(person) // { _name: 'zhangsan', age: 18, sex: 'male', eat: [Function: eat] }
-
属性拼接
let firstName = 'ai'; let secondName = 'xiaoye'; let _name = 'ai xiaoye'; let person = { [firstName + secondName]: _name, // [firstName + 'xiaoye']: _name, // ['ai' + 'xiaoye']: _name, } console.log(person); // { aixiaoye: 'ai xiaoye' }
-
对象的解构
结构一致,属性名称一致
let { a: a, b: b, c: c } = { a: 1, b: 2, c: 3 }; // 简写 let { a, b, c } = { a: 1, b: 2, c: 3 }; console.log(a, b, c); // 1 2 3
-
不完全解构
值多了,变量少了
let { a = 2, b, c } = { a: 1, b: 2, c: 3, e: 4, f: 5 }; console.log(a, b, c); // 1 2 3 // 默认值 let { a = 2, b, c } = { b: 2, c: 3, e: 4, f: 5 }; console.log(a, b, c); // 2 2 3
-
解构失败
变量多了,值少了
let { a = 2, b, c, d, e, f, g, h } = { b: 2, c: 3, e: 4, f: 5 }; console.log(a, b, c, d, e, f, g, h); // 2 2 3 undefined 4 5 undefined undefined
-
小问题
// 数组的结构存在顺序的问题 // let [d, [e], [f]] = [1, [2], [3]]; // console.log(d,e,f) // 1 2 3 let [d, [e], [f]] = [2, [1], [3]]; console.log(d,e,f) // 2 1 3 // 对象的结构不存在顺序的问题 // let { a, b, c } = { a: 1, b: 2, c: 3 }; // console.log(a, b, c) // 1 2 3 let { a, b, c } = { c: 3, b: 2, a: 1 }; console.log(a, b, c) // 1 2 3
-
现象
const {son} = person;
我们经常看到这样的解构,其实就是拿取person里的son属性,然后与取出来的变量同名
4.4 实战
-
拿到JSON里面的course数据
// 思路:首先结构要一致,然后用一个变量来接收 var data = [{ "id": 1, "course": "前端开发", "classes": 100, "teacher": "小野" }, { "id": 2, "course": "后端开发", "classes": 120, "teacher": "小夏" }, { "id": 1, "course": "全栈开发", "classes": 200, "teacher": "哈默" }]; let [{ course: course1 }, { course: course2 }] = data; console.log(course1, course2); // 前端开发 后端开发
-
取到wangwu儿子
var pserson = { name: "张三", age: 50, son: { name: 'lisi', age: 30, son: { _name: 'wangwu', age: 12 } } } let { son: { son } } = pserson; console.log(son) // { _name: 'wangwu', age: 12 } let { son: { son: son1 } } = pserson; console.log(son1) // { _name: 'wangwu', age: 12 }
这里其实就是多层解构,把第一层解构的结果拿到然后以:{属性}或:{属性:变量名}的形式再继续解构
5. 解构本质、()用法、解构其他用法、函数参数解构、解构隐式转换、解构书写规范
5.1 解构本质
解构赋值本质就是给变量赋值,赋值的方式就是模式匹配
5.2 ()用法
-
解构赋值对象如果不let声明,JS就认为它是一个语法块(块级作用域)
let a; { a } = { a: 1 }; console.log(a) // 报错 JS认为{}是一个语法块
解决办法:用括号包裹,变成表达式
let a; ({ a } = { a: 1 }); console.log(a) // 1
只有这种情况加括号不会报错,取余情况均会报错
-
当用 let、var 的方式来声明,只要加了括号就报错
加了括号就是表达式,语法不通过,所以报错
let [(a)] = [1]; let {(a):b} = {}; let ({a:(b)}) = {}; let ({a:b}) = {}; // 全报错
-
函数的形参也不能加括号
因为上一节课已经得出结论:函数的形参也是用let声明的,所以加上括号就会报错
function foo(([z])) { return z; } console.log(foo([1])); //报错
-
模式匹配必须l模式一样才能匹配
[b] = [3]; console.log(b); // 3 ([b]) = [3]; console.log(b); // 报错 匹配规则不对,所以匹配不成功 ({a: (b) = {}}) console.log(b); // {} 没有匹配 JS认为{a: (b) = {}}是一个对象,a是属性,b是属性值 默认值为{} var a = {}; [(a.b)] = [3]; // 以数组的匹配模式 给a对象的b属性赋值3 console.log(b); // 报错 匹配了 但必须通过a.b访问
-
报错:let is not defined
一般情况下就是因为 let = (…) 导致的错误,let不能声明表达式
5.3 解构其他用法
-
模式匹配可以给对象的属性赋值
let a1 = [1, 2, 3], obj2 = {}; [obj2.a, obj2.b, obj2.c] = a1; console.log(obj2.a, obj2.b, obj2.c); // 1,2,3
-
计算属性解构
数组是特殊的对象,也支持对象的匹配规则
let arr = [1, 2, 3]; let { 0: first, [arr.length - 1]: last } = arr; console.log(first, last); // 1,3
字符串拼接属性
let a = 'x', b = 'y', obj = {}; // { a: obj[a + b] } = { a: 2 } 这里不通过的原因是它认为{}是一个块级作用域,所以要把它变成表达式,这样才能解构 ({ a: obj[a + b] } = { a: 2 }); console.log(obj.xy); // 2
-
交换值
let a = 10, b = 20; [b, a] = [a, b] console.log(a, b); // 20 10
-
模式匹配允许匹配同源属性(同一个源属性)
只要匹配的源属性存在,就可以写多个相同的属性
但是变量不能相同,因为这样就重复声明了
let { a: x, a: y } = { a: 1 }; console.log(x, y); // 1 1 let { a: x, a: x } = { a: 1 }; console.log(x, x); // 报错,重复声明
-
解构的默认参数
在解构中使用对象或是数组的时候,慎用(可读性非常差)
var x = 200, y = 300, z = 100; var obj1 = { x: { y: 42 }, z: { y: z } }; ({ y: x = { y: y } } = obj1); // x = {y: 300} ({ z: y = { y: z } } = obj1); // y = {y: 100} ({ x: z = { y: x } } = obj1); // z = {y: 42} console.log(x, y, z); // { y: 300 } { y: 100 } { y: 42 }
-
注意
({ x = 10 } = {}); // x => x:x=10 ({ y } = { y: 10 }); // y => y:y console.log(x, y); // 10 10
5.4 函数参数解构
-
数组解构
function test([x, y]) { console.log(x, y); } test([1, 2]); // 1, 2 test([1]); // 1 undefined test([]); // undefined undefined
-
对象解构
function test({ x, y }) { console.log(x, y); } test({ x: 1, y: 2 }); // 1, 2 test({ x: 1 }); // 1 undefined test({}); // undefined undefined
-
默认参数
注意除了函数有默认值,解构参数也有默认值
function foo({ x = 10 } = {}, { y } = { y: 10 }) { console.log(x, y); } foo() // 10 10 foo({},{}) // 10 undefined foo({x:2},{y:3}) // 2 3
5.5 解构隐式转换
字符串、number、boolean都会进行隐式转换
-
字符串转为类数组
const [a, b, c, d, e] = 'hello'; // 这里将字符串隐式转换成了类数组 console.log(a, b, c, d, e) // h e l l o // 验证 let { length: len } = 'hello'; console.log(len); // 5
-
数值转换相应的包装类
let { toString: s } = 123; console.log(s); // [Function: toString] // 验证 console.log(s === Number.prototype.toString); // true
-
布尔值转换相应的包装类
let { toString: s } = false; console.log(s); // [Function: toString] // 验证 console.log(s === Boolean.prototype.toString); // true
-
undefined和null不能进行隐式转换
let { prop } = undefined; console.log(prop); // 报错 let { prop } = null; console.log(prop); // 报错
包装类其实就是JS底层经过加工的对象,方便隐式转换
5.6 解构书写规范
建议使用缩进增强代码可读性
let person = {
name: 'zs',
ageZhang: 50,
son: {
name: 'ls',
ageLi: 30,
son: {
name: 'ww',
ageWang: 18,
son: {
name: 'zl',
ageZhao: 0
}
}
}
}
// 不要这样写
let { son: { son: { son: { ageZhao }, ageWang }, ageLi }, ageZhang } = person;
console.log(ageZhao, ageWang, ageLi, ageZhang);
// 规范写法
let {
son: {
son: {
son: {
ageZhao
},
ageWang
},
ageLi
},
ageZhang
} = person;
6. 函数默认值特殊情况、this指向回顾、箭头函数、rest运算符
6.1 函数默认值特殊情况
-
length
函数一旦给了默认值,length会发生变化,体现在默认值所给的位置
function test(a, b, c) {} test(); console.log(test.length); //输出3 function test(a, b, c = 1) {} test(); console.log(test.length); //输出2 function test(c = 1, a, b) {} test(); console.log(test.length); //输出0 function test(a, b, c = 1, d, e, f) {} test(); console.log(test.length); //依然是2
发现length不会计算给默认值的形参及后面的形参
-
映射关系
函数一旦给了默认值,实参和形参的映射关系就会失效
function test(a, b, d, e, f) { arguments[1] = 7; console.log(b); // 输出7 形参和实参是映射的关系 } test(1, 2, 3, 4, 5, 6) function test(a, b, d, e, f = 1) { arguments[1] = 7; console.log(b); // 输出2 一旦给了默认值就会造成映射失效 } test(1, 2, 3, 4, 5, 6);
-
形参解构
如果解构没有匹配到,那么赋值为默认参数
如果解构匹配到了,那么赋值为匹配上的值
如果解构undefined,那么就会报错
function foo({x, y = 5}){ console.log(x, y); } foo({}); //undefined 5 foo({x:1});//1 5 foo({x:1,y=2})//1 2 foo();//报错 // 解决报错 function foo({x, y = 5}={}){ console.log(x, y); } foo();//undefine 5 如果没有传值为undefined,就会找默认值进行解构
-
fetch请求
如果没有传配置对象,那么就会与默认值{}进行模式匹配,从而避免报错
function fetch(url,{body = "",method = "get"} = {}) fetch('http://www.baidu.com'); // 配置对象就应用了默认值
-
默认值作用域
-
调用栈
函数在调用的时候会创建一个调用栈(call stack),这个调用栈记录着函数所调用的位置(可以借助控制台的source工具查看)
可以通过控制台的调用栈查看函数的作用域的赋值情况
-
示例
y函数里的x赋值给了形参x
var x = 1; function foo(x, y = function() { x = 2; console.log(x); }) { // 这里是外层作用域,函数y也产生了作用域,它与外层形成了闭包 var x = 3; // 这里是里层作用域 y(); console.log(x); } foo(); console.log(x); // 输出2 3 1 // 模拟 var x = 1; { let x = 2; let y = function() { x = 2; console.log(x) }; { let x = 3; y(); console.log(x); } } console.log(x); // 输出 2 3 1
-
6.2. this指向回顾
- 默认绑定规则:函数内部this默认指向window、严格模式下没有this指向
- 隐式绑定:谁调用指向谁,全局调用指window
- 显示绑定:call(obj,a,b,c)、apply(obj,[a,b,c])、bind(obj,a,b,c),前两个是函数执行,后者是函数本身
- new 构造函数最后一步this指向实例对象
优先级:4 > 3 > 2 > 1
6.3 箭头函数
-
各种写法
-
基本形式
() => {}
-
参数只有一个
如果形参只有一个可以省略() 、返回值只有一个表达式可以省略{}
a => a;
-
没有参数的情况
没有形参时必须要有()
() => a;
-
返回对象
简写返回对象时可以包上括号,否则报错, 因为JS认为是语法块
(a, b) => ({a:1, b:2});
-
-
与解构赋值来结合
const full = ({first,last} = {} ) => first +''+last; console.log(full({first:3,last:5}))//输出3,5
-
简化排序
var arr = [123,12,31,23,1,4,4213,3213,43]; var arr1 = arr.sort((a,b) => a - b) // 即可排序
-
不存在arguments
var sum = (a, b) => { console.log(arguments); // 报错 return a + b; } sum(1, 2)
6.4 rest运算符
-
概念
...
可叫做rest/spread运算符 ,用于展开或收集数据 -
收集
形式:… + 变量
//将值收集为数组 var sum = (...args) =>{ log(args[0]+args[1]); // 3 } sum(1,2) //收集必需是最后一个参数 let fn =(a,b,...c) =>{ console.log(a,b,c); // 1,2,3,[4,5,6,7] } fn(1,2,3,4,5,6,7)
-
展开
形式:… + 数组
//将数字展开为各个值 function foo(x, y, z){ console.log(x, y, z) // 1 2 3 } foo(...[1,2,3]) foo(...[1,2,3,3,4,5]) //不影响 //有点类似apply, 也是以数组传参 function foo(x, y, z){ console.log(x, y, z) // 1 2 3 } foo.apply(undefined,[1,2,3,4,5]); // 让this指向不发生改变 foo.apply(null,[1,2,3,4,5]); //特殊用法 let a = [2, 3, 4]; let b = [1,...a,5]; // [1,2,3,4,5]
-
简化排序
// es5 function sortNum() { //这里slice起到把类数组转成数组的作用 return Array.prototype.slice.call(arguments).sort(function(a, b) { return a - b }); } console.log(sortNum(12, 12, 34, 5, 22, 56, 7, 78)); // es6 const sortNum = (...args) => args.sort((a, b) => a - b); console.log(sortNum(12, 12, 34, 5, 22, 56, 7, 78));
-
length
函数形参长度不包括rest运算符和默认值
cosnole.log((function(a){}).length);//1 cosnole.log((function(...a){}).length);//0 cosnole.log((function(b,...a){}).length);//1 cosnole.log((function(b,c,...a){}).length);//2 cosnole.log((function(b,c,...a,d=0){}).length);//报错 cosnole.log((function(b,...a,a=0){}).length);//1 length不再准确
7. 箭头函数的实质、箭头函数的使用场景
7.1. 箭头函数的实质
this由外层的函数作用域来决定(箭头函数的作用域是父级的作用域,不是父级)
不能作为构造函数来使用(因为没有this)
call、apply、bind无效(因为没有this)
没有arguments对象(因为与function不是同一类型函数,用拓展运算符代替)
yield 命令不能生效,在generator函数中(因为没有this)
function foo(){
console.log(this)
return (a) =>{
console.log(this.a) // 这里是一个闭包
}
}
var obj1 = {a:2};
var obj2 = {a:3};
var bar = foo.call(obj1);
bar.call(obj2)//输出的是2
const person = {
eat(){
log(this);
},
drink:()=>{
log(this);
}
}
person.eat(); // {eat:f,drink:f} 调用隐式绑定了this
person.drink(); // Window 箭头函数没有普通函数this的四个特性
7.2. 箭头函数的使用场景
-
事件绑定
箭头函数省去了bind的操作
(function() { function Button() { this.Button = document.getElementById('button'); } Button.prototype = { init() { this.bindEvent(); }, bindEvent() { // es5写法 this.Button.addEventListener('click', this.clickBtn.bind(this), false); //默认参数就是事件对象 // es6写法 this.Button.addEventListener('click', (e) => this.clickBtn(e), false); // 回调里传事件对象 }, clickBtn(e) { console.log(e); console.log(this); } } new Button().init(); })();
-
嵌套
箭头函数内部并没有自己的this,只能通过父级作用域来获取this(闭包的this)
function foo(){ return () =>{ return ()=>{ return ()=>{ console.log('id',this.id) } } } } var f = foo.call({id: 1}); var f1 = f.call({id:2})()(); // id 1 箭头函数没有this,所以没法call var f2 = f().call({id:3})(); // id 1 var f3 = f()().call({id:4}); // id 1
-
arguments
箭头函数没有arguments,下面的题是一个坑
function foo(){ setTimeout(()=>{ console.log(arguments); //1234567 }) } foo(1,2,3,4,5,6,7); //这里拿的是父级作用域的arguments
-
闭包(Closure)
一个函数的执行,导致另一个函数的定义,会形成闭包
-
链式调用
链式调用时避免箭头函数,可读性较差
// es5 function insert(value){ // 插入的数字 return{ into:function(array){ // 数组 return{ after:function(afterValue){ // 在第几位插入 arr.splice(array.indexOf(afterValue)+1,0,value); return array; } } } } } log(insert(5).into([1,2,3,4,6,7,8]).after(4));//12345678 // es6 let insert = (value) =>({ into:(array) =>({ after:(afterValue)=>{ array.splice(array.indexOf(afterValue)+1,0,value) return array; } }) })
7.3 总结
-
返回的值唯一
-
内层函数需要调用外层this
-
类数组转化为数组时
// es5 function sortNum() { //这里slice起到把类数组转成数组的作用 return Array.prototype.slice.call(arguments).sort(function(a, b) { return a - b }); } // es6 const sortNum = (...args) => args.sort((a, b) => a - b);
-
链式调用、嵌套的时候避免使用箭头函数,可读性较差
8. 获取函数名、对象拓展、静默失败、描述符、getter/setter
8.1 获取函数名
-
name
var f = function (){} console.log(f.name); //es5 输出"" es6输出f
-
anonymous
console.log(new Function().name); console.log((new Function).name); //输出anonymous
-
bound foo
call/apply就不行,因为会执行
function foo(){} console.log(foo.bind({}).name); //输出bound foo
8.2 对象拓展
-
属性简写
变量和属性相同可简写
const foo = "bar"; const baz = {foo}; //等同于{foo:foo} console.log(baz);//{foo: "bar"}
function foo(a, b){ console.log({a,b}) //等同于{a:a,b:b} } foo(1, 2);//{a:1,b:2}
-
方法简写
const person = { age: '12', say(){console.log(this.age);} } person.say(); //12 let age = '12'; const person = { age, say(){console.log(this.age);} } person.say(); //12
-
属性拼接
let obj = {}; obj.foo = true; obj['f'+'o'+'o'] = false; console.log(obj)//{ foo: false } let a = 'hello'; let b = 'world'; let obj = { [a+b]:true, ['hello'+b]:123, ['hello'+'world']: undefined } console.log(obj) //{helloworld:undefined}
-
属性是非字符串
会有相应的包装,隐式转化为字符串
var myObject = {}; myObject[true] = 'foo'; myObject[3] = 'bar'; myObject[myObject] = 'baz'; console.log(myObject['true']); //输出foo console.log(myObject['3']); //输出bar console.log(myObject[myObject]); //输出baz console.log(myObject["[object Object]"]); //输出baz const a = {a:1}; const b = {b:2}; const obj = { [a]: 'valueA', [b]: 'valueB' } console.log(obj) //会经过包装类 输出{[object Object]:"valueB"}
对象属性会隐式转换成字符串,即调用toString,对象toString后是[object Object]
-
获取方法名
const person = { sayName(){ console.log('hello'); } } console.log(person.sayName.name); //sayName
8.3 描述符
-
getOwnPropertyDescriptor
获取该属性的属性描述符
let obj = {a: 2}; console.log(Object.getOwnPropertyDescriptor(obj,'a')); //参数:对象、属性(字符串) // 下面输出的就是描述配符置对象、里面的就是描述符: // { // configurable: true, //可配置(包括删除) // enumerable: true, //可遍历 // value: 2, //属性值 // writable: true //可写 // }
-
defineProperty
定义属性,并可配置该属性的属性描述符
let obj = {}; Object.defineProperty(obj, 'a', { //参数:对象、属性(字符串)、属性描述配置 value: 2, enumerable: true, writable: true, configurable: true }) console.log(obj.a); //2 console.log(Object.getOwnPropertyDescriptor(obj, 'a')); //注意这是Object构造器上的方法,无法通过原型链继承 // 输出 { value: 2, writable: true, enumerable: true, configurable: true }
-
静默失败
操作不生效,但是不报错
严格模式下会报错,普通模式下不报错
"use strict" let obj = {}; Object.defineProperty(obj, 'a', { value: 2, enumerable: true, writable: false, configurable: true }) obj.a = 3; //不能修改 console.log(obj.a); //2 delete obj.a; //但是可以删,需要配置configurable: false才可以阻止删除 console.log(obj); //{}
8.4 getter/setter
-
默认操作
[[Get]]:就是在获取属性时默认会执行的操作
[[Set]]:就是在修改属性时默认会执行的操作
获取属性和修属性的底层原理:
let obj = {a: 1}; obj.a; //获取属性[[Get]](默认操作),查找当前属性如果没有,查找原型 obj.a = 3; //赋值操作[[Put]](默认操作) // 步骤: // 1. getter/setter // 2. writable: false, 不让你改 // 3. 赋值
如果定义了getter和setter操作,就会覆盖原本的 [[Get]] 和 [[Set]] 操作
-
GET
-
概念
可以重写[[Get]]默认操作,访问属性时调用
-
基本形式
var obj = { log:['example','test'], get latest(){ // 用get方式定义了一个获取属性的默认操作 if(this.log.length === 0)return undefined; return this.log[this.log.length-1] } } console.log(obj.latest);//test
伪属性:用get方式重写的获取属性的默认操作叫做伪属性
-
注意事项
属性描述配置在有get情况下:
- value和writable是不可用的(功能重复),否则会报错
- configuable和enumerable可以用
var myObject = { get a() { return 2; } } Object.defineProperty(myObject, 'b', { get: function() { return this.a * 2; }, enumerable: true, // value: 6, // writable: ture // value和writable开启会报错 }); console.log(myObject.a); // 2 console.log(myObject.b); // 4
-
-
set
-
概念
可以重写[[Put]]默认操作,修改属性时调用
-
基本形式
var language = { set current(name){ //这里必要要给参数 this.log.push(name) }, log:[] } language.current = 'EN'; //这里值就是set的参数 language.current = 'FA'; console.log(language.log); //[EN,FA];
-
注意事项
get与set一般情况下都是成对出现
var obj = { // _a:undefined, get a(){ return this._a; }, set a(val){ return this._a = val *2 } } obj.a = 3; console.log(obj.a);//6
-
9. 查看getter/setter函数名、对象密封4种方式、is方法、assign合并与覆盖、assign拷贝、无法拷贝getter/setter的问题、浅拷贝、浅拷贝新方式、部署对象的方式
9.1 查看 getter/setter 对应的函数名
可以通过 getOwnPropertyDescriptor 查看
const obj = {
get foo(){},
set foo(x){}
}
var descriptor = Object.getOwnPropertyDescriptor(obj,'foo'); // 参数:目标对象、属性名称
console.log(descriptor.get.name);//打印get foo
console.log(descriptor.set.name);//打印set foo
另外,getter和setter可以叫做取值函数和存值函数
9.2 对象密封4种方式
-
对象常量
-
概念
对象常量即属性不可删除和修改
当一个对象的属性描述配置为configurable: false, writable: false的时候就把这个对象叫做对象常量
obj = { a:2 } Object.defineProperty(obj,'a',{ value: 6, configurable: false, // 说明不可删除 writable: false, // 说明不可修改 enumerable: true });
-
缺点
可以添加属性
-
-
preventExtensions/isExtensible
-
禁止对象的拓展
var obj = {a: 2}; Object.preventExtensions(obj); obj.b = 3; console.log(obj);//{a:2}
-
查看是否可拓展
log(Object.isExtensible(obj)); //false 不可拓展
-
特性
.
添加属性:会静默失败,严格模式下会报错,属性描述符默认是truedefineProperty
添加属性:不会静默失败,直接报错,属性描述符默认是false
'use strict' obj.b = 6; // 报错 Object.defineProperty(obj,'b',{ value: 6, }); // 直接报错
-
缺点
可以删除属性
-
-
seal/isSealed
-
原理
底层调用了preventExtensions,且把所有的configurable改为false
-
密封对象
var obj = {a : 2}; Object.seal(obj);
-
查看是否密封
console.log(Object.isSealed(obj))//true
-
缺点
可以修改属性
-
-
freeze/isFrozen
-
冻结对象
此方法密封级别最高,属性不能增删改
var obj = {a : 2}; Object.freeze(obj);
-
查看是否冻结
console.log(Object.isFrozen(obj));//为true则代表已经冻结
-
深度冻结
对象里面是对象的情况就需要深度冻结了
function myFreeze(obj){ Object.freeze(obj); for(var key in obj){ if(typeof(obj[key]) === 'object' && obj[key] !== null){ myFreeze(obj[key]) } } }
-
9.3 is方法
除了下面两种情况,和===没什么区别
console.log(NaN == NaN);//false
console.log(+0 == -0);//true
console.log(Object.is(NaN,NaN))//true
console.log(Object.is(+0,-0))//false
9.4 assign
-
概念
用于合并一个对象,返回对象是目标对象(同一个引用)
-
基本使用
Object.assign(目标对象,原对象);
let obj = {a: 1}; let tar = {}; let copy = Object.assign(tar,obj); log(copy);//{a: 1} log(copy === tar) //true log(copy === obj) //false // 证明assign返回的对象就是原对象
-
合并与覆盖
-
合并
const tar = {a: 1}; const tar2 = {b: 2}; const tar3 = {c: 3}; Object.assign(tar,tar2,tar3) log(tar);//{a:1,b:2,c:3}
-
覆盖
const tar = {a:1,b:1}; const tar2 = {b:2,c:2}; const tar3 = {c:3} // 自下向上覆盖 Object.assign(tar, tar2, tar3); console.log(tar);//{a:1,b:2,c:3}
-
-
特殊参数
-
第一个参数为原始值
要求是对象,不是则会隐式转化
undefined和null无法进行合并因为没有包装类
Object.assign(undefined,{a:1}); //报错 Object.assign(null,{a:1}); //报错
原始值会隐式转化成对象,所以可以合并
var test = Object.assign(1,{a:1}); //Number{1,a:1} var test = Object.assign(true,{a:1}); //Boolean{1,a:1} var test = Object.assign("1",{a:1}); //String{1,a:1}
-
第二个参数为原始值
要求是可枚举的对象,不是则会隐式转化
字符串经过隐式转化后就是一个可枚举的对象,所以可以合并
var test = Object.assign({a:1},"123"); //{0: '1', 1: '2', 2: '3', a: 1}
以下经过隐式转化成对象后没有可遍历的属性,剩下的都是私有变量,所以相当于合并一个空对象,合并后就等于返回原对象
Object.assign({a:1}, undefined); //{a:1} 没有包装类,不会报错,相当于合并了一个空 Object.assign({a:1}, null); //{a:1} var test = Object.assign({a:1},1); //{a:1} var test = Object.assign({a:1},true); //{a:1}
-
-
拷贝Object.create()
-
create
用于生成一个对象,且可以指定原型
-
拷贝
原型属性和不可枚举属性都不能拷贝
var obj = Object.create({foo:1},{ // 参数:原型属性配置、对象属性配置(属性描述符) bar:{ value:2 }, baz:{ value:3, enumberable:true } }) console.log(obj) // { // baz:3 // bar:2 // __proto__: // foo:1 // } var copy = Object.assign({},obj); console.log(copy);//{baz:3}
-
-
拷贝symbol()
-
symbol
可以生成一个永远都不会重复的字符串
var a = symbol(); var b = symbol(); console.log(a === b);//打印false var a = symbol('a'); var b = symbol('a') console.log(a == b)//打印false
-
拷贝
var test = Object.assign({a:'b'},{[Symbol('c')]:'d'}); //symbol返回的是symbol类型,包上[]里面就会有包装类,隐式转换toString console.log(test); //{a:"b",Symbol(c):"d"}
-
-
浅拷贝
assign只能浅拷贝对象
const obj1 = {a:{b:1}}; const obj2 = Object.assign({},obj1); obj1.a.b = 2; // 修改了obj1, obj2也会跟着被改,说明拷贝的是引用,这就是浅拷贝 console.log(obj2);//{a:{b:2}}
-
替换相关
-
同名属性替换
assign只能替换整个属性,无法替换单个深入替换
const target = {a:{b:'c',d:'e'}}; const sourse = {a:{b:'hello'}}; Object.assign(target, sourse); console.log(target);//{a:{b:hello}}
-
数组的替换
对象根据属性名替换,数组根据索引替换(隐式转换后索引就是属性名)
var a = Object.assign([1, 2, 3],[4,5]); console.log(a);//输出[4,5,3]
-
-
扩充属性和方法
往原型上扩充属性和方法
var age = 1; function Person(){} Object.assign(Person.prototype,{ eat(){}, age, }) console.log(Person.prototype); // {age: 1, eat: ƒ, constructor: ƒ}
-
默认值
通过三个参数来实现
const DEFAULT = { url: { host: 'www.baidu.com', port: 7070 } } function test(option){ option = Object.assign({},DEFAULT,option); // 参数: 空对象,默认对象,配置对象 console.log(option); } // 传参 test({url:{port:8080}}); //{url:{host:'www.baidu.com',port:8080}} // 不传参 test(); //{url:{host:'www.baidu.com',port:7070}}
9.5 无法拷贝getter/setter的问题
-
问题描述
由于assign无法拷贝getter和setter(取值函数和赋值函数),于是defineProperties/getOwnPropertyDescriptors就诞生了,它们的存在就是为了解决这个问题 。
const sourse = { get foo(){ return 1; } } const target = {}; Object.assign(target,sourse); console.log(target); //{foo:1} //这样就不行 const sourse = { set foo(value){ console.log(value); } } const target = {}; Object.assign(target,sourse); console.log(target); //{foo:undefined}
-
defineProperties/getOwnPropertyDescriptors
-
defineProperties
定义多个属性
var obj = {} Object.defineProperties(obj,{ a:{ value: true, writable: true }, b:{ value: 'hello', writable: false } })
-
getOwnPropertyDescriptors
获取多个属性描述符
console.log(Object.getOwnPropertyDescriptors(obj)); // { // a: // configurable: false // enumerable: false // value: true // writable:true // } // { // b: // configurable: false // enumerable: false // value: 'hello' // writable:true // }
-
-
解决方案
使用defineProperties拷贝(定义)多个属性,再通过
Object.getOwnPropertyDescriptors(sourse)
配置多个属性描述配置对象,从而实现拷贝的效果(实则是重新定义了属性)-
setter拷贝
const sourse = { set foo(value){ console.log(value); } } const target = {}; Object.defineProperties(target, Object.getOwnPropertyDescriptors(sourse)); console.log(Object.getOwnPropertyDescriptors(sourse)) //输出: // { // foo: { // get: undefined, // set: [Function: set foo], // enumerable: true, // configurable: true // }, // } console.log(Object.getOwnPropertyDescriptor(target,'foo')); // 输出: // { // configurable: true // enumerable: true // get: undefined // set: ƒ foo(value) // } console.log(target); //输出: // { // set foo: ƒ foo(value) // 这里set foo是自定义属性,呈浅紫色 // [[Prototype]]: Object //}
-
getter拷贝
const sourse = { get foo(){ return 1; } } const target = {}; Object.defineProperties(target, Object.getOwnPropertyDescriptors(sourse)); console.log(Object.getOwnPropertyDescriptors(sourse)) // 输出: // { // foo: { // get: [Function: get foo], // set: undefined, // enumerable: true, // configurable: true // }, // } console.log(Object.getOwnPropertyDescriptor(target,'foo')); // 输出: // { // configurable: true // enumerable: true // get: ƒ foo() // set: undefined //} console.log(target); //输出 // { // foo: (...), // (...)说明描述符配置了get // get foo: ƒ foo(), // [[Prototype]]: Object // }
-
9.6 浅拷贝新方式
传统的浅拷贝只能拷贝其属性,这种方法除了属性还能拷贝原型
var obj = {a:1, b:2, c:3}
// 拷贝obj原型 拷贝obj属性
const clone = Object.create(Object.getPrototypeOf(obj),Object.getOwnPropertyDescriptors(obj))
console.log(clone) // {a:1,b:2,c:3}
//优化写法
const clone = (obj) => Object.create(Object.getPrototypeOf(obj),Object.getOwnPropertyDescriptors(obj));
clone1(obj); // {a:1,b:2,c:3}
9.7 部署对象的方式
-
对象字面量
const obj = {foo:123}
-
create方法
const obj = Object.create({protoKey:'protoVal'}); obj.foo = 123;
-
assign方法
const obj = Object.assign(Object.create({protoKey:'protoVal'}),{ foo: 123; })
-
create、getOwnPropertyDescriptors组合
const obj = Object.create({protoKey:'protoVal'},getOwnPropertyDescriptors({foo:123}))
10. 更改原型、key/value/entries遍历、super、symbol遍历、for和keyFor、几种遍历方式对比
10.1 更改原型
-
__ proto__
- 属于内部属性,原则上不允许修改
- 访问效率慢
- 所有继承子该原型的对象都会影响到
var pserson = new Person(); person.__proto__ = {} // 替代方式: // Object.setPrototypeOf() 写的操作 // Object.getPrototypeOf() 读取操作 // Object.create() 生成操作
-
setPrototypeOf/getPrototypeOf
-
概念
setPrototypeOf(obj, proto):设置原型,第一个参数是目标对象,第二个参数是原型对象
getPrototypeOf(obj):查看原型,参数为要查看的对象
-
参数
-
第一个参数是对象
let proto = { y: 20, z: 40 } let obj = {x: 10}; let obj1 = Object.setPrototypeOf(obj,proto); console.log(obj1 === obj) // true; console.log(obj)
输出:
-
第一个参数是原始值
只会经过一次包装类,原型不会有改变
let obj = Object.setPrototypeOf(1,{}); console.log(obj); //1
let obj = Object.setPrototypeOf(1,{a:1,b:2}); // 这里1经过了包装类,即Number(1) console.log(Object.getPrototypeOf(obj));
输出:
这里1的原型其实就是Number的原型,而Number的原型原始值默认就是0,并且第二个参数并没有放到原型上,说明这里的1直接经过了包装类,然后忽略了后面原型的设置
-
-
10.2 4种遍历方式
-
key
遍历对象属性,
不能遍历原型属性和不可枚举属性
const foo = { a: 1, b: 2, c: 3 } Object.defineProperties(foo,{ d:{ value: 4, enumerable: true }, f:{ value: 5, enumerable: false } }) console.log(Object.keys(foo)); // ['a','b','c','d']
-
value
遍历对象的值,
不能遍历原型属性和不可枚举属性
console.log(Object.value(foo)); // [1,2,3,4]
-
entries
把对象属性和值遍历成数组,
不能遍历原型属性和不可枚举属性
console.log(Object.entries(foo)); // [["a",1], ["b",2], ["c",3], ["d",4]]
-
原始值遍历
原始值只能遍历字符串,因为字符串包装类后属性是可枚举的
let obj = '123' console.log(Object.keys(obj)); //[0,1,2] //1和true 都是[] //un和null会报错
10.3 super
用于指向对象的原型,使用有很多限制,必须用对象方法的简写形式才能使用super调用
let proto = {
y: 20,
z: 40,
bar(){
this.y
}
}
let obj = {
x: 10,
foo(){
console.log(super.y);
},
doo(){
super.bar();
}
}
Object.setPrototypeOf(obj,proto);
obj.foo();//20
// 原型的方法也能访问
obj.doo();//20
10.4 symbol遍历
-
概念
解决对象的属性出现重名的问题
-
特点
- 是原始值
- 所有的symbol值都不一样
- 不是构造函数
- 识别用标识符
- 不能挂属性
-
注意点
-
不是构造函数,不能new
-
值是独一无二的
let s1 = Symbol(); let s2 = Symbol(); console.log(s1 === s2);//false
-
也是原始值,属于symbol类型
let s1 = Symbol(); let s2 = Symbol(); log(typeof s1);//symbol
-
挂不上属性
let sq = Symbol(); sq.a = 1;//挂不上属性 console.log(sq.a)//输出undefined
-
识别通过标识符识别
let s1 = Symbol('foo') // 这样写标识符是无效的,因为还没有登记
-
有包装类,转化成字符串
var obj = {a: 1}; let s1 = Symbol(obj); console.log(s1); //Symbol([object Object]) //如果传入null则是symbol(null) //undefined则是 symbol()
-
Symbol不能转成Number类型,会报错
var sy = Symbol() console.log(String(sy)) //'Symbol()' console.log(!sy) //false console.log(sy + 1) //报错 隐式转换成Number
-
可以通过
Object.getPrototypeOf
查看原型let s1 = Symbol(); console.log(s1.toString()); //'Symbol()' 可以直接用.调用上面的方法 console.log(Object.getPrototypeOf(s1));
-
写入对象的方式
//写法一 let name = Symbol(); let person = {}; person[name] = 'zhangsan'; console.log(person); //{Symbol():"zhangsan"} //必须通过symbol来声明, .name就是字符串访问了所以不行 //写法二 let name = Symbol(); let person = { [name]: 'zhangsan' }; console.log(person);//{Symbol():"zhangsan"} //写法三 let person = {} let name = Symbol(); Object.defineProperty(person, name, { value: 'zhangsan' }) console.log(person) //{Symbol():"zhangsan"} console.log(person.name) //undefined console.log(person[name]) // 'zhangsan' //写法四 let name = Symbol(); let eat = Symbol(); let person = { [name]: 'zhangsan', [eat](){ console.log(this[name]) } } person[name](); // 'zhangsan'
-
-
for和keyFor
-
for
用于登记symbol的标识符(未登记的标识符不会有效,登记了才会生效)
let s1 = Symbol('foo'); let s2 = Symbol('foo'); log(s1 === s2);//false 因为标识符还没有登记,所以不同 let s1 = Symbol.for('foo'); let s2 = Symbol.for('foo'); log(s1 === s2);//true s1和s2登记的标识符一致,所以相同 let s1 = Symbol('foo'); // 未登记 let s2 = Symbol.for('foo'); // 登记了 log(s1 === s2);//false
-
keyfor
获取标识符(只能获取登记的标识符)
let s1 = Symbol('foo'); let s2 = Symbol.for('foo'); console.log(Symbol.keyFor(s1));//undefined console.log(Symbol.keyFor(s2));//foo
-
10.5 几种遍历方式对比
- for in遍历:只能遍历对象本身可枚举的属性和原型属性(不包含symbol属性)
- keys遍历:只能遍历对象本身可枚举的属性(不包含原型,symbol属性)
- getOwnPropertySymbols遍历:只能遍历对象本身的Symbol属性(包含对象不可枚举的Symbol属性,不包含原型属性)
- assign遍历(合并):只能遍历对象本身可枚举的属性和Symbol属性(不包含对象不可枚举的Symbol属性,不包含原型属性)
const obj = {}
let a = Symbol('a');
let b = Symbol('b');
obj[a] = 'hello';
obj[b] = 'world';
obj.c = 'hh';
for(let i in obj){
log(i)
}
// {c:haha}
var obj1 = {}
Object.assign(obj1 ,obj);
console.log(Object.getOwnPropertySymbols(obj));
// [
// 0:Symbol(a)
// 1:Symbol(b)
// ]
10.6 综合练习
const obj = {c:1, d:2};
let a = Symbol('a');
let b = Symbol('b');
let _a = Symbol('_a');
let _b = Symbol('_b');
obj[a] = 'hello';
obj[b] = 'world';
Object.defineProperties(obj,{
e:{
value: 5,
enumerable: true
},
f:{
value: 6,
enumerable: false
},
[_a]:{
value: -1,
enumerable: true
},
[_b]:{
value: -2,
enumerable: false
},
})
let h = Symbol('h');
let i = Symbol('i');
let j = Symbol('j');
const obj1 = {
g: 7,
[h]: 8
}
Object.defineProperties(obj1,{
[i]:{
value: 9,
enumerable: true
},
[j]:{
value: 10
},
k:{
value: 11
}
})
Object.setPrototypeOf(obj, obj1)
console.log(obj)
// {
// c: 1
// d: 2
// e: 5
// f: 6 //灰色
// Symbol(a): "hello"
// Symbol(b): "world"
// Symbol(_a): -1
// Symbol(_b): -2 //灰色
// __proto__:
// g: 7
// k: 11 //灰色
// Symbol(h): 8
// Symbol(i): 9
// Symbol(j): 10 //灰色
// __proto__: Object
// }
for(let i in obj){
console.log(i)
}
//c d e g
console.log(Object.keys(obj))
// [
// 0: "c"
// 1: "d"
// 2: "e"
// ]
console.log(Object.getOwnPropertySymbols(obj))
// [
// 0: Symbol(a)
// 1: Symbol(b)
// 2: Symbol(_a)
// 3: Symbol(_b)
// length: 4
// ]
var obj3 = {}
Object.assign(obj3,obj);
console.log(obj3)
// {
// c: 1
// d: 2
// e: 5
// Symbol(a): "hello"
// Symbol(b): "world"
// Symbol(_a): -1
// }
JSON.stringify(); //遍历对象可枚举的属性和symbol属性(不包含原型)
11. 箭头函数/rest运算符拓展、Symbol、迭代器初识、数据结构、迭代器实现、typeArray、forOf
11.1. 箭头函数/rest运算符拓展
-
箭头函数不能修改this指向
function foo() { return () => { // 第一层 return () => { // 第二层 return () => { // 第三层 console.log('id:', this.id); } } } } var f = foo.call({ id: 1 }); // 最外层是普通函数,所以可以修改this指向 var t1 = f.call({ id: 2 })()(); // 第一层是箭头函数,所以无法修改this指向 var t2 = f().call({ id: 3 })(); // 第二层是箭头函数,所以无法修改this指向 var t3 = f()().call({ id: 4 }); // 第三层是箭头函数,所以无法修改this指向 // 输出:1 1 1 // 解决办法:把箭头改成普通函数 // 与箭头函数不同的是,普通函数的this默认指向window,而不会往上找
-
在es2017上面增加了对对象的拓展和展开
var obj = { a: 1, b: 2, c: 3 } var obj1 = { a: 4, d: 5, e: 6 } var obj2 = { } // 传统写法 Object.assign(obj2, obj, obj1); // tip:相同的属性会被后面所覆盖 console.log(obj2) // { // a:4 // b:2 // c:3 // d:5 // e:6 // } // es2017写法 var obj2 = { ...obj ...obj1 } // 结果和上面一致
11.2. Symbol
var s = Symbol();
console.log(Object.getPrototypeOf(s));
输出:
原理:每当用户使用这些方法,就会隐式转化成这种形式
FOO[Symbol.hasInstance](foo)
自动帮你去调用,Symbol.hasInstance相当于是方法属性,很少会用到这些方法,在开发框架或者库的时候可能会用到
11.3. 迭代器初识
-
迭代器(iterator)
打开数组原型,会发现最后面有一个迭代器的方法
Symbol(Symbol.iterator): f values()
,它就是通过Symbol的方式来部署的通过隐式的标准化接口可以访问迭代器函数,即
arr[Symbol.iterator]()
let arr = [1, 2, 3, 4] let iter = arr[Symbol.iterator] // 通过把Symbol.iterator作为属性,访问iterator函数 console.log(iter) // f values{[native code]} let iter = arr[Symbol.iterator](); // 返回iterator实例 consoe.log(iter); //Array Iterator {} //[[Prototype]]: Array Iterator //next: ƒ next() //Symbol(Symbol.toStringTag): "Array Iterator" //[[Prototype]]: Object
通过
.next()
对数据结构进行抽取迭代,返回当前成员的信息对象(value:迭代抽取的值,done:迭代是否完成)console.log(iter.next()); //{value: 1, done: false} value:迭代抽取的值,done:迭代是否完成 console.log(iter.next()); //{value: 2, done: false} console.log(iter.next()); //{value: 3, done: false} console.log(iter.next()); //{value: 4, done: false} console.log(iter.next()); //{value: undefined, done: true}
迭代器就是对数据解构读取的一种方式,一种有序的,连续的,基于拉取的一种消耗数据的组织方式(消耗就是指只会读取一次,以后都不会访问)
11.4 数据结构
-
概念
把数据放入相应的数据结构的当中方便管理和操作
-
分类
- 数组:Array
- 对象:Object
- 类数组:arguments、nodeList、Map、Set、weakMap、weekSet
- 类型数组:TypeArray(二进制数据的缓存区,类似于数组,不是类数组)
以上除了对象都可以用迭代器进行迭代
11.5. 迭代器实现方式
function makIterator(array){
var nextIndex = 0;
return { // 这里是一个闭包,被抛出去的函数与这里的作用域捆绑在了一起(相当于函数内部缓存了nextIndex这个变量)
next: function(){
return nextIndex < array.length ?
{value: array[nextIndex++], done:false}:
{value: undefined,done:true}
}
}
}
var iter = makeIterator([1,2,3,4]);
console.log(iter.next()); //{value: 1, done: false}
console.log(iter.next()); //{value: 2, done: false}
console.log(iter.next()); //{value: 3, done: false}
console.log(iter.next()); //{value: 4, done: false}
console.log(iter.next()); //{value: undefined, done: true}
11.6 typeArray
-
概念
typeArray是处理二进制数据的,属于类型数组,由于JS里面没有定义这个构造函数,所以无法查看,但是可以看到二进制数据的构造函数
// JS中没有TypeArray这个构造函数 console.log(TypeArray);// 报错
-
二进制数据
声明一个空间用来存储二进制数据
// 二进制数据的构造函数 const tArray = new Int8Array(8); // 声明了一个8进制数据 console.log(tArray); // Int8Array(8) [0, 0, 0, 0, 0, 0, 0, 0, buffer: ArrayBuffer(8), byteLength: 8, byteOffset: 0, length: 8, Symbol(Symbol.toStringTag): 'Int8Array'] // 赋值 tArray[0] = 100; console.log(tArray); // Int8Array(8) [100, 0, 0, 0, 0, 0, 0, 0, buffer: ArrayBuffer(8), byteLength: 8, byteOffset: 0, length: 8, Symbol(Symbol.toStringTag): 'Int8Array']
11.7 forOf
-
概念
由于迭代器的调用比较繁琐,ES6参考了C++,JAVA,C#,Python的 for…of,为部署过 iterator 接口的数据类型提供了一种简单的、统一的遍历接口
只要部署了 iterator 接口的数据类型都可以通过 for…of 来进行迭代(底层就是直接调用了 iterator 接口)
-
调用方式
let arr = [1, 2, 3, 4] fo(let i of arr){ console.log(i); // i 就是迭代的值 } //1 2 3 4
-
for…in与for…of的区别
for…in:用于遍历对象、数组(取得的是属性、索引)
for…of:用于迭代部署过 iterator 接口的数据类型(取得的是迭代抽取的值)
对象不在迭代的范畴之内,因为它不是有序的、也不是连续的
-
对象部署迭代器接口
如果要迭代对象就要部署iterator接口,否则会报错
let obj = { start:[1, 3, 2, 4], end:[5,7,6] } for(var i of obj){ console.log(i); // 报错 }
部署迭代器:
let obj = { start:[1, 3, 2, 4], end:[5,7,6], [Symbol.iterator](){ // 这里包中括号是为了隐式转化成字符串 let index= 0, arr = [...this.start,...this.end], len = arr.length return{ next(){ if(index<len){ return{ value: arr[index++], done: false } }else{ return{ value: undefined, done: true } } } } } } for(var a of obj){ console.log(a); // 1 3 2 4 5 7 6 } // 每一次的迭代都是独立的,所以上一次迭代完毕不会影响下一次迭代 var iter = obj[Symbol.iterator](); console.log(iter.next()) // { value: 1, done: false } console.log(iter.next()) // { value: 3, done: false } // 如果没有部署迭代器接口是无法展开的 console.log(...obj) //报错 // 如果有迭代器接口 console.log(...obj) //1 3 2 4 5 7 6
12. 数组构造器新增方法(of、from)、数组原型新增方法(fill、keys/values/entries、copyWithin、find/findIndex、includes)、数值拓展(进制的表示、数值方法调整、数值新增方法 )、Math拓展
12.1. 数组构造器新增方法
-
of
-
概念
声明数组
-
存在的意义
用Array创建数组时只传一个值会有歧义
console.log(new Array(3)) //[empty*3] 预期是填充3,却填充了3个空值
为了解决这个问题,es6新增了数组方法of,它可以把一个数值填充进数组
console.log(Array.of(3)); //[3]
从此除了字面量、构造函数,又多了一种声明数组的方式:of
-
-
from
-
概念
它能够将类数组,或部署了 iterator 接口的对象转化成数组(纯对象会转化成空数组)
-
基本用法
-
传统写法:
var oList = document.querySelectorAll('p'); console.log([].slice.call(oList)); //[p,p,p]
调用数组的slice方法,修改其this指向指向类数组,不作切片,即可转化成一个数组
-
新增写法:
console.log(Array.from(oList)); //[p,p,p]
部署了 iterator 接口的对象也可转化
let obj = { start: [12,3,4,5], end: [7,8,9], [Symbol.iterator](){ let index = 0, arr = [...this.start,...this.end], // 实际上就是拿的合并的数组 len = arr.length; return { next(){ if(index<len){ return { value: arr[index++], done: false } }else{ return { value: undefined, done: true } } } } } } console.log(Array.from(obj)); //[12,3,4,5,7,8,9]
有三个参数 arrayLike | mapFn | thisArg,分别是类数组 | map回调 | this指向 (几乎不用)
console.log(Array.from(obj,function(val,idx){ return val * 2 })); // [24, 6, 8, 10, 14, 16, 18]
mapFn何时调用?在类数组转化成数组后即刻调用
-
-
12.2. 数组原型新增方法
-
fill
-
概念
arr.fill(value, start [0], end [this.length] )
往数组里面填充值,第一个参数指定填充的值,后面两个参数并可以指定填充的起始位置,且修改原数组
-
基本用法
-
若不指定填充起始位置默认填充全部
let arr1 = [1, 2, 3, 4]; var arr = arr1.fill(5); console.log(arr); // [5, 5, 5, 5] console.log(arr1); // 结果和上面一致,说明修改原数组
-
起始位置是属于左闭右开
let arr1 = [1, 2, 3, 4]; arr1.fill(5, 1, 2); console.log(arr1); // [1, 5, 3, 4] arr1.fill(5, 1, 1); console.log(arr1); // 不作处理
-
特殊值
- 负值
let arr1 = [1, 2, 3, 4]; arr1.fill(5, -3, -2); console.log(arr1); // [1, 5, 3, 4]
- NaN
let arr1 = [1, 2, 3, 4]; arr1.fill(5, NaN, -2); // 当作是0 console.log(arr1); // [5, 5, 3, 4] arr1.fill(5, NaN, NaN); console.log(arr1); // 不作处理
- 负值
-
操作对象
let arr1 = [1, 2, 3, 4]; console.log([].fill.call({length: 3}, 4)); // {0: 4, 1: 4, 2: 4, length: 3}
-
-
-
keys/values/entries
对象与数组keys/values/entries的区别:
-
对象上的keys/values/entries —— 返回值是数组
let obj = { a: '1', b: '2', c: '3' } console.log(Object.keys(obj)); // ["a","b","c"]; console.log(Object.values(obj)); // ["1","2","3"]; console.log(Object.entries(obj)); // [["a","1"],["b","2"],["c","3"],length:3]; let ObjectKeys = Object.keys(obj); // => Object.values(obj) => Object.entries(obj) for(let i = 0; i < ObjectKeys.length; i++){ console.log(ObjectKeys[i]); } for(let i of ObjectKeys){ console.log(i) } // 依次打印: // a b c // 1 2 3 // ["a","1"] ["b","2"] ["c","3"]
-
数组上的keys/values/entries —— 返回值是迭代器实例
let arr = ['a','b','c']; console.log(arr.keys(obj)); // Array Iterator {}; console.log(arr.values(obj)); // Array Iterator {}; console.log(arr.entries(obj)); // Array Iterator {}; let iter = arr.keys(obj); // => arr.values(obj) => arr.entries(obj) for(let i of iter){ console.log(i) } // 依次打印: // 0 1 2 // a b c // [0,"a"] [1,"b"] [2,"c"]
-
-
copyWithin(了解)
-
概念
arr.copyWidth(target[0], start[0], end(this.length))
能够复制数组内部的成员,第一个参数指定复制起点,后面两个参数指定复制的起始位置,会修改原数组(和fill比较类似)
-
基本用法
-
若不指定填充起始位置默认复制全部
var arr = [1,2,3,4,5,6,7]; arr.copyWithin(2); console.log(arr); // [1, 2, 1, 2, 3, 4, 5]
-
起始位置是属于左闭右开
var arr = [1,2,3,4,5,6,7]; arr.copyWithin(0,3); console.log(arr); // [4, 5, 6, 7, 5, 6, 7] arr.copyWithin(0,3,4); console.log(arr); // [4, 2, 3, 4, 5, 6, 7]
-
负值
var arr = [1,2,3,4,5,6,7]; arr.copyWithin(-2); console.log(arr); // [1, 2, 3, 4, 5, 1, 2] arr.copyWithin(-2,-3,-1); console.log(arr); // [1, 2, 3, 4, 5, 5, 6]
-
操作对象
// 指定位置替换(根据键名来匹配) console.log([].copyWithin.call({length:5,3:1},0,3)); // {0:1,3:1,length:5} console.log([].copyWithin.call({length:5,3:1,4:1,5:1,6:1},0,3)); // {0:1,1:1,2:1,3:1,4:1,5:1,6:1,length:5} (替换和length没有任何关系) // 全部填充 console.log([].fill.call({length:3},4)); // {0:4,1:4,2:4,length:3}
-
-
-
find/findIndex
-
find
寻找符合条件的第一个数组成员,找不到NaN
var arr =[1,2,3,4]; var arr1 = arr.find(function(value,index,arr){ return value > 2 }) console.log(arr1); // 3
找不到返回undefined
-
findIndex
寻找符合条件的第一个数组成员的索引,可以找到NaN
var arr =[1,2,3,4]; var value = arr.find(function(value,index,arr){ return value > 2 }) console.log(value); // 3 var index = arr.findIndex(function(value){ return value > 2; }); console.log(index); // 2
找不到返回-1
弥补了indexOf的不足 —— NaN不等于NaN
console.log([NaN].indexOf(NaN)); // -1; console.log([NaN].findIndex(y => Object.is(NaN, y))); // 0;
-
-
includes
简化版的find
判断数组当中是否包含指定成员(包括NaN),返回一个布尔值
console.log([1,2,3].includes(2)); // true console.log([1,NaN,3].includes(NaN)); // true
12.3. 数值拓展
es6对数值新增了数值的表示以及方法,调整了数值的位置
Tip:数值就是指Number数据类型
-
进制的表示
-
16进制 —— 0x
console.log(0x1f7); // 503
-
8进制 —— 0o
console.log(0o767); // 503
-
2进制 —— 0b
console.log(Number.prototype.toString.call(503,2)); // 111110111 console.log(0B111110111); // 503
-
10进制
console.log(parseInt(111110111,2)); // 503
通过这些前缀可以把2/8/16进制转成10进制,通过toString可以把10进制转成2/8/16进制
-
-
数值方法调整
把全局上的数值方法迁移到了Number的构造器上,并作了相应调整
-
isNaN 方法
判断数值是否为NaN,全局上使用会隐式转换,迁移后修正了// 隐式转化 console.log(isNaN('NaN')); // true // 修正了 console.log(Number.isNaN('NaN')); // false
-
isFinite 方法
判断数值是否有有限,全局上使用会隐式转换,迁移后修正了console.log(isFinite(42)); // true // 一般非数都是无限的 console.log(isFinite(NaN)); // false console.log(isFinite(infinity)); // false // 隐式转化 console.log(isFinite('42')); // true // 修正了 console.log(Number.isFinite('42')); // false
-
parseInt/parseFloat 也作了迁移,用法和全局一致
-
-
数值新增方法
-
isInteger
判断数值是否为整数
console.log(Number.isInteger(24)); // true console.log(Number.isInteger(24.0)); // true JavaScript引擎认为整数和后面是0的浮点数的是同样的存储方法 console.log(Number.isInteger(24.1)); // false
-
isSafeInteger
判断是否在安全整数的区间 [-9007199254740991, 9007199254740991]
// 安全整数等于计算器的最大使用范围(2^53-1) console.log(Number.MAX_SAFE_INTEGER === Math.pow(2,53)-1);//true console.log(Number.MAX_SAFE_INTEGER === -Math.pow(2,53)+1);//true console.log(Number.isSafeInteger(1)); // true console.log(Number.isSafeInteger(1.1)); // false 不是处理范围内的整数
可以通过Number.prototype找到另一些数值的最大范围
-
12.4 Math拓展
Math是一个内置的对象,没有构造器,看不到原型,如果要看上面的方法的话可以直接打印Math对象
数学对象也新增了一些方法,但不怎么会用到,用到的时候可以去官网查:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Math
13. 正则新增特性(声明方式、方法调整、修饰符yus、新增方法、UTF_16编码方式)
13.1 正则回顾
-
修饰符
- global -g
- ignoreCase -i
- mutli-line -m
-
元字符
- \w \W word 查找字母和数字
- \d \D digit [0-9] 查找数字
- \s \S space(\n换行 \r回车 \t制表 \v垂直换行 \f分页)查找空白字符
- \b \B bridge 匹配单词边界
- . 除了\n \r \u2028 \u2029所有字符
-
贪婪模式
能匹配多,就不匹配少
-
正向预查(先行断言)(先行否定断言)
例:x后面紧跟着y
/x(?=y)/
13.2 正则新增特性
-
声明正则的变化方式
-
传统声明方式
var reg = /xyz/ig var reg = new RegExp('xyz', 'ig'); var reg = new RegExp(/xyz/gi);
-
新增声明方式
var reg = new RegExp(/xyz/gi, 'gm'); // 如果两边都存在修饰符,则以后面为准
以前es5这样写会报错
-
-
字符串上的正则方法进行了调整:将字符串上的正则方法调整到了正则原型上
RegExp.prototype[Symbol.match]; RegExp.prototype[Symbol.replace]; RegExp.prototype[Symbol.search]; RegExp.prototype[Symbol.split]; // 当字符串调用这些正则方法的时候,实际是调用了正则原型上的方法 console.log(String.prototype.match); console.log(String.prototype.replace); console.log(String.prototype.search); console.log(String.prototype.split);
-
新增的修饰符 y u s
-
y(sticky)粘黏
与g的对比,主要体现在exec:
var str = 'aaa_aa_a'; var reg1 = /a+/g; var reg2 = /a+/y; // 第一次exec console.log(reg1.exec(str));//['aaa',index:0,input:'aaa_aa_a'] console.log(reg2.exec(str));//['aaa',index:0,input:'aaa_aa_a'] // 第二次exec console.log(reg1.exec(str));//['aa',index:4,input:'aaa_aa_a'] console.log(reg2.exec(str));//null
匹配的内容必须粘黏在一起,如果中间隔了东西,那么往后的都无法匹配,返回null
-
u(unicode)
可以识别编码极限以上的字符
-
码点(UTF-16编码):每个码点可以存储两个字节
U+0000 ~ U+D800 是一些常用的字,以两个字节表示一个字符(‘\u20bb7’: 超出了编码极限)
U+D800 ~ U+FFFF 是一些生僻的字,以四个字节表示一个字符(‘\u20bb7’改成’\uD842\uDFB7’: 𠮷,也可用’\u{2pbb7}'代替,JS会帮忙解析)JS里面的字符就是用16进制编码来进行编译存储的,经过字符编译后代码就叫做码点,它是Unicode代码空间中的一个值,Unicode也叫做字符总码,里面有17种编码格式(每种编码格式可存放2^16个字符),其中utf-16就是其中一种
-
es5不支持四个字节表示一个字符
console.log(/^\uD83D/.test('\uD83D\uDC2A'))//true es5只能识别D800一下的字符(认为\uD83D是一个字符) console.log(/^\uD83D/u.test('\uD83D\uDC2A'))//false -u可以识别D800以上的字符(认为\uD83D\uDC2A是一个字符)
-
.
修饰符不能匹配超出编码极限的字符var s = '\uD842\uDFB7'; console.log(s); //𠮷 console.log(/^.$/.test(s)); //false 超出了编码极限无法匹配了 console.log(/^.$/u.test(s)); //true
-
{}
修饰符有两个作用:当作量词、识别超出编码极限的编码
那么使用的如何区分呢?
// 这样写表示量词 console.log(/a{2}/.test('aa')); // true; console.log(/a{2}/u.test('aa')); // true; // 这样写表示编码 console.log(/\u{20bb7}/u.test('𠮷')); // true
-
-
s(dotAll)
匹配所有字符(包括\n换行,\r回车,\u2028行分隔符,\u2029段分隔符)
console.log(/foo.bar/.test('foo\nbar')); //false console.log(/foo.bar/s.test('foo\nbar')); //true console.log(/foo.bar/s.dotAll); //true
-
-
新增方法
-
查看修饰符是否定义:
var reg = new RegExp('xyz','ig'); console.log(reg.global) // true g console.log(reg.ignoreCase) // true i console.log(reg.multiLine) // false m console.log(reg.dotAll) // false s console.log(reg.sticky) // false y console.log(reg.unicode) // false u
-
查看正则主体和修饰符
var reg = /\wabed/giy; console.log(reg.source); // wabed console.log(reg.flags); // giy
-
14. 字符串拓展(字符表示、码点相关方法(length、charAt、charCodeAt 、codePointAt、forOf、fromCharCode、fromCodePoint)、字符串新增方法(includes / startsWith / endsWith、repeat、padStart / padEnd))、模板字符串(模板渲染、注入问题、标签模板)
14.1. 字符串拓展
-
UTF-16编码回顾
在JS里面的字符是以utf-16编码方式进行解析
utf-16的编码范围:
- U+0000 ~ U+D800:两个字节表示一个字符
- U+D800 ~ U+FFFF:属于空位,四个字节表示一个字符(超出编码极限的字符会以这种方式解析)
超出编码极限的字符也可用花括号包裹,JS会帮忙解析,例如:\u{2pbb7} == 𠮷
-
字符的表示方式
-
代码补全
如果码点不足4位,则用0补全
console.log('\u0041\u0042\u0043'); // ABC
-
{}解析
可以解析超出编码极限,或者没有补全的码点
console.log('\u{0041}\u{0042}\u{0043}'); // ABC console.log('\u{41}\u{42}\u{43}'); // ABC console.log('\u{2pbb7}'); // 𠮷 console.log('\uD842\uDFB7' === '\u{20BB7}'); //true
-
-
码点相关方法
-
length
码点长度就是字符长度
var s = "\u{20BB7}"; // == \uD842\uDFB7 console.log(s.length); // 2
这样的处理方式不符合预期,只对应了一个字符,长度应该是1,而不是2
-
charAt(es5)
输出索引所对应的字符
缺点:不能正确处理超出编码极限的字符
console.log(s.charAt(0)); // � console.log(s.charAt(1)); // �
-
charCodeAt (es5)
输出索引所对应的码点(10进制)
缺点:不能正确处理超出编码极限的字符
console.log(s.charCodeAt(0)); // 55362 console.log(s.charCodeAt(0)); // 57271 // 转成16进制 console.log(Number.prototype.toString.call(55362,16)); // D842 console.log(Number.prototype.toString.call(57271,16)); // DFB7
-
codePointAt(es6)
输出索引所对应的码点(10进制)
能正确处理超出编码极限的字符,并能拿到索引对应的码点
var s= "𠮷a" console.log(s.codePointAt(0)); // 134071 console.log(Number.prototype.toString.call(55362,16)); // 20bb7 console.log(s.length); // 3 长度还是3,只是识别被正确的解析了 console.log(s.codePointAt(0));//134071 超出编码极限 => 正确解析 console.log(s.codePointAt(1));//57271 console.log(s.codePointAt(2));//97 console.log(Number.prototype.toString.call(97,16));//61 console.log('\u0061'); //a console.log('\u{61}'); //a
应用:判断一个字符是2个字节还是4个字节组成
function is32Bit(c){ // 1Byte == 8Bit return c.codePointAt(0) > 0xffff; // 转16进制为10进制 } console.log(is32Bit('吉')); //true console.log(0xffff); //655535 比对的时候直接转为10进制比较
-
forOf
可以正确的输出超出编码极限的字符
var s = "𠮷a"; // 对比一下 for (let i = 0; i < str.length; i++) { console.log(str[i]); // � � a } for (let value of s) { console.log(value); // 𠮷 a }
-
fromCharCode(es5)
传入一个码点返回一个对应的字符
缺点:不能识别超出编码极限的码点
console.log(String.fromCharCode(0x20bb7)); // ஷ // 处理方式:当超出物理极限的时候舍弃最高位,返回相应的字符 console.log(String.fromCharCode(0x20bb7) === String.fromCharCode(0x0bb7)); //true
-
fromCodePoint(es6)
传入一个码点返回一个对应的字符
能识别超出编码极限的码点
String.fromCodePoint(0x20BB7); //吉
-
-
字符串新增方法
-
includes / startsWith / endsWith
判读一个字符串是否在另一个字符串当中、开头、结尾
let s = "Hello world!"; console.log(s.includes("o")); // true console.log(s.startsWith('Hello')); // true console.log(s.endsWith('!')); // true
-
repeat
把原本的字符串重复N次
console.log('x'.repeat(3)); //xxx console.log('x'.repeat(2.9)); //xx console.log('x'.repeat(NaN)); // 空 console.log('x'.repeat(0)); // 空 console.log('x'.repeat("3")); //xxx 会有隐式转换
-
padStart / padEnd
开头、结尾填充 指定字符串 至 指定长度
console.log('x'.padStart(5,"ab")); //ababx console.log('x'.padStart(4,"ab")); //abax console.log('x'.padEnd(4,"ab")); //xaba console.log('x'.padEnd(5,"ab")); //xabab
-
14.2. 模板字符串
-
基本用法
let name = 'web' let info = 'developer' let m = `I am a ${name} ${info}`; console.log(m); //I am a web developer
-
进阶用法
模板里面是表达式,可以做很多事情:
-
可以运算
let x = 1; let y = 2; console.log(`${x} + ${y} = ${x+y}`) // 1 + 2 = 3
-
可以调用对象
let obj = {x:1,y:2}; console.log(`${obj.x + obj.y}`) // 3
-
可以调用函数
function fn(){ return [1,2,3,4]; } console.log(`foo ${fn()} bar`) //foo 1,2,3 bar
模板里会隐式转换成字符串
-
可以嵌套字符串
let msg = `Hello, ${'place'}`; console.log(msg); //Hello, place
-
-
模板渲染方法
const temp = arr1 => ` <table> ${ arr1.map(addr => ` <tr><td>${addr.first}</td></tr> <tr><td>${addr.last}</td></tr> `) } </table> ` const data = [ {first:"zhang",last:"san"}, {first:"li",last:"si"}, ] console.log(temp(data));
输出:
发现有个逗号,这是因为map返回的是一个数组,而模板内部会隐式转换成字符串,所以导致了这个结果
解决方法:
${ arr1.map(addr => ` <tr><td>${addr.first}</td></tr> <tr><td>${addr.last}</td></tr> `).join('') }
用join方法把数组拼接起来就好了
-
注入问题
一些恶意的模板数据会通过这种方式渲染到页面
const data = [ {first:"zhang",last:"<script>alert('abc')</script>"}, {first:"li",last:"si"}, ]
-
标签模板
专门用于解决恶意注入的问题
通过标签模板 tag`` 的形式调用函数,可以在函数内部做相应的处理
参数依次是:以{}分隔的字符串,往后{}里的值
let a = 5; let b = 10; tag `Hello ${a+b} world${a*b}`; function tag($,$1,$2){ console.log($,$1,$2) } //['Hello ',' world',''] 13 50
解决方案:
function SaferHTML(tempData) { let s = ''; for (let i = 1; i < arguments.length; i++) { let arg = String(arguments[i]); s += arg.replace(/</g, "<").replace(/>/g, ">") } return s; } let sender = '<script>alert("abc")<\/script>' let message = SaferHTML `<p>${sender} has set you message</p>` console.log(message);
输出:
注意区分:字符串里的实体符是不会被浏览器解析的,标签里的实体符才会被解析
15. map与set、set对比map、set/map对比array/object
15.1 新增数据结构
-
set:类似于数组,只能存值没有键,值是唯一的
-
map:类似于对象,可以存键和值,键名不限于字符串,键和值一一对应
set、map、Promise、Proxy四个无法通过babel编译语法降级(poly feel、babel poly feel)
15.2 set
-
原型
var set = new Set(); console.log(set); // Set(0) {size: 0} // [[Entries]] // No properties // size: 0 // [[Prototype]]: Set // add: ƒ add() // clear: ƒ clear() // constructor: ƒ Set() // delete: ƒ delete() // entries: ƒ entries() // forEach: ƒ forEach() // has: ƒ has() // keys: ƒ values() // size: (...) // values: ƒ values() // Symbol(Symbol.iterator): ƒ values() // Symbol(Symbol.toStringTag): "Set" // get size: ƒ size() // [[Prototype]]: Object
-
参数
参数是具备 iterator 接口的数据结构:[],类数组
var set = new Set([5,7]); console.log(set); // Set(2) {5, 7} // [[Entries]] // 0: 5 // 1: 7 // size: 2 // [[Prototype]]: Set
-
声明方式
//方法一 var set = new Set([1,2,3]); console.log(set); //Set(3) {1,2,3} //方法二 var set = new Set(); set.add(1).add(2).add(3); console.log(set); //Set(3) {1,2,3}
-
值是唯一的
var set = new Set([1,2,3,4,5,5,5,5]); console.log(set); //Set(5) {1,2,3,4,5}
-
特殊值
var set = new Set([undefined,undefined,null,null,5,'5',true,1,NaN,NaN,{},{},[],[]]) console.log(set); // Set(11) {undefined, null, 5, '5', true, …} // [[Entries]] // 0: undefined // 1: null // 2: 5 // 3: "5" // 4: true // 5: 1 // 6: NaN 注意:1. 在set里面NaN是等于NaN的 // 7: Object 2. {},[]是构造器实例出来的唯一引用,所以不等 // 8: Object // 9: Array(0) // 10: Array(0) // size: 11 // [[Prototype]]: Set
-
操作方法
-
add
追加数据,返回值是set实例var set = new Set(); var x = {id:1}; var y = {id:2}; //一般用法 set.add(x); set.add(y); //链式调用 set.add(x) // 返回值是set实例本身 .add(y) .add(x); // 引用相同,无法添加 console.log(set); // Set(2) {{…}, {…}} // [[Entries]] // 0: // value: {id: 1} // 1: // value: {id: 2} // size: 2 // [[Prototype]]: Set
-
size
返回当前长度console.log(set.size); // 2
-
delete
清除某个值,返回值是布尔值,操作是实时的console.log(set.delete(y)); // false console.log(set); // Set(2) {{…}, {…}} // [[Entries]] // 0: // value: {id: 1} // [[Prototype]]: Set
-
clear
清空所有的值,返回值是undefined,操作是实时的// 注意打印位置: 发现输出结果一致,说明操作是实时的 console.log(set); // Set(0) {size: 0} // [[Entries]] // No properties // size: 0 // [[Prototype]]: Set set.clear() console.log(set); // Set(0) {size: 0} // [[Entries]] // No properties // size: 0 // [[Prototype]]: Set
-
has
判断是否有指定值,返回值是布尔值console.log(set.has(x)); // true
-
-
遍历方法
-
keys/values/entries
遍历键,值,键值对数组,返回值是迭代器对象
set 不存在键名,故键名和键值是一致的
let set = new Set([1, 2, 3, 4, 5, 6, 7]); console.log(set.keys()); // SetIterator {1, 2, 3, 4, 5, …} console.log(set.values()); // SetIterator {1, 2, 3, 4, 5, …} console.log(set.entries()); // SetIterator {1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5, …} for (let i of set.keys()) { console.log(i); // 1 2 3 4 5 6 7 } for (let i of set.values()) { console.log(i); // 1 2 3 4 5 6 7 } for (let i of set.entries()) { console.log(i); // [1,1] [2,2] [3,3] [4,4] [5,5] [6,6] [7,7] }
-
for…of
set对象上也部署了迭代器,可通过for…of遍历// 判断遍历的是键还是值,结果是一致的,说明实际上是在调用set实例上的values方法 console.log(Set.prototype[Symbol.iterator] === Set.prototype.values);//true for(let values of set){ console.log(values); //1 2 3 4 5 6 7 }
-
forEach
遍历成员(键、值、set结构)let set = new Set(['a', 'b', 'c', 'd']); set.forEach((value, keys, s) => { console.log(value, keys, s) }) // a a Set(4) {'a', 'b', 'c', 'd'} // b b Set(4) {'a', 'b', 'c', 'd'} // c c Set(4) {'a', 'b', 'c', 'd'} // d d Set(4) {'a', 'b', 'c', 'd'}
-
-
使用场景
-
拓展运算符-set 结构转化为数组,去重
去重最简单的方式就是收集到set当中,再展开到一个数组里
// ...能展开具备迭代器接口的数据结构 let set = new Set(['a','b','c','d','e','f','g']); console.log(...set); //a b c d e f g let set = new Set(['a','b','c','d','e','f','g']; console.log([...set]); //['a','b','c','d','e','f','g']
-
map-操作成员
let set = new Set([1,2,3,4,5,6,7]) let set1 = new Set([...set].map(value => value*2)); console.log(set1); // Set(7) {2, 4, 6, 8, 10, …}
难点
var arr = [1,2,3,4] var arr1 = arr.map(parseInt); console.log(arr1);//[1,NaN,NaN,NaN] //原理就是map里的参数 value, index,默认传给了parseInt var arr1 = arr.map((value,idx)=> console.log(value,idx)); //1 0 //2 1 //3 2 //4 3 //那么就代表着,第二个参数转换进制,无法转换就是NaN parseInt(1,0)parseInt(2,1)parseInt(3,2)parseInt(4,3); let set2 = new Set([[...set].map(parseInt)]); //这里是数组里嵌套数组,所以size是1 console.log(set2); // Set(1) {Array(7)} // [[Entries]] // 0: Array(7) // value: Array(7) // 0: 1 // 1: NaN // 2: NaN // 3: NaN // 4: NaN // 5: NaN // 6: NaN // length: 7 // [[Prototype]]: Array(0) // size: 1 // [[Prototype]]: Set
-
filter-过滤成员
let set2 = new Set([...set].filter(x => (x%2) == 0)) console.log(set2); //{2,4,6}
-
-
交集并集差集
let a = new Set([1, 2, 3]); let b = new Set([4, 3, 2]); //并集 let union = new Set([...a, ...b]); console.log(union); //Set(4) {1, 2, 3, 4} //交集 let intersect = new Set([...a].filter(x => b.has(x))); console.log(intersect); //Set(2) {2, 3} //差集 let difference = new Set([...a].filter(x => !b.has(x))); console.log(difference) //Set(1) {1}
-
映射新的结构
//方法一 let set = new Set([1,2,3]); let set1 = new Set([...set].map(value => value*2)); //写法二 let set = new Set([1,2,3]); let set1 = new Set(Array.from(set,value=>value*2));
15.3 map
-
原型
和set的原型基本一致,只是多了set、get方法,主要用于存取键值,而set本身没有键,所以不需要这两个方法
-
与对象对比
-
普通对象
键和值不能实现一一对应
var m = {} var x = {id:1}, y = {id:2}; m[x] = "foo"; // => m['object Object'] = "foo"; m[y] = "bar"; // => m['object Object'] = "bar"; console.log(m); //{[object Object]:"bar"} console.log(m[x]); //bar console.log(m[y]); //bar //当键名是对象的时候会调用toString从而发生覆盖
-
map结构
键和值一一对应
let m = new Map(); var x = {id:1}, y = {id:2}; m.set(x,'foo'); // Tips: 1.这里是通过set设置的 m.set(y,'bar'); console.log(m.get(x));//foo 2. 这里通过get访问的 console.log(m.get(y));//bar
-
-
参数
- 参数是具备 iterator 接口的数据结构:[],类数组
- 参数是以键值对存在的二维数组
[['键名', '键值'], ...]
let m = new Map([ ['name', 'zhangsan'], ['title', 'lisi'] ]) console.log(m) // Map(2) {'name' => 'zhangsan', 'title' => 'lisi'} // [[Entries]] // 0: {"name" => "zhangsan"} // 1: {"title" => "lisi"} // size: 2 // [[Prototype]]: Map
-
声明方式
// 方法一 let m = new Map([ ['name', 'wangwu'], ['title', 'zhaoliu'] ]) //传参实现原理: var items = [['name','wagwu'],['title','zhaoliu']] let m = new Map(); items.forEach(([key,value]) => m.set(key,value)) // 方法二 let m = new Map(); m.set('name','zhangsan') m.set('title','lisi')
-
覆盖问题
-
原始值
const map = new Map(); // 地址相同所以会覆盖 map.set(1, 'foo'); // Map(1) {1 => 'foo'} map.set(1, 'bar'); // Map(1) {1 => 'bar'} // 认定-0和+0是全等的,只是Object.is判断是false map.set(-0,123); console.log(map.get(+0)) //123 // 不会有隐式转换 map.set(true,1); map.set('true',2); console.log(true); // 1 // 键名也可以是undefined和unll map.set(undefined,1) map.set(null,2) console.log(map.get(undefined)); //1 console.log(map.get(null)); //2 // 认定NaN是等于NaN的,只是Object.is判断是true map.set(NaN,123); console.log(map.get(NaN));//123
-
引用值
// 指针不同所以访问不到 map.set([5],555); console.log(map.get([5])); // undefined // 指针相同所以可以访问 var arr = [5]; map.set(arr,555); console.log(map.get(arr)); //555
原始值在栈里面存储的是地址,引用值在栈里面存储的是指向堆的指针
-
-
操作方法
- set
添加成员,返回值set实例本身const m = new Map(); // 链式调用 m.set(1,"foo").set(2,"bar"); console.log(m); // Map(2) {1 => 'foo', 2 => 'bar'}
- get
获取成员,返回值set实例本身m.get(1); //'foo'
- size
获取成员长度var x = {id:1}, y = {id:2}; m.set(x,"foo"); m.set(y,"bar") ; cosnole.log(m.size); //2
- delete
删除成员,返回值是布尔值,操作是实时的var x = {id:1}, y = {id:2}; m.set(x,"foo"); m.set(y,"bar") ; cosnole.log(m.delete(x)); //true m.delete(x); // Map(1) {{...} => "bar"}
- clear
清空成员,返回值undefined,操作是实时的var x = {id:1}, y = {id:2}; m.set(x,"foo"); m.set(y,"bar") ; cosnole.log(m.clear()); //undefined m.clear(); // Map(0) {}
- has
判断成员,返回值是布尔值var x = {id:1}, y = {id:2}; m.set(x,"foo"); m.set(y,"bar") ; cosnole.log(m.has(x)); //true
- set
-
遍历方法
- keys/values/entries
var x = {id:1}, y = {id:2} m.set(x,"foo") m.set(y,"bar") for(let keys of m.keys()){ console.log(keys) } // {id:1} // {id:2} for(let keys of m.values()){ console.log(values) } // foo // bar for(let keys of m.entries()){ console.log(entries) } // [{...},'foo'] // [{...},'bar']
- for…of
console.log(m[Symbol.iterator] === m.entries) //true 说明实际在调用map实例上的entries方法 for(let i of m){ console.log(i) } // [{...},"foo"] // [{...},"bar"] // 模式匹配 for(let [key,values] of m){ console.log(key,values) } // {id:1}"foo" // {id:2}"bar"
- keys/values/entries
-
使用场景
-
拓展运算符-map 结构转化为数组
const myMap = new Map(); myMap.set(true,7) .set({foo:3},['abc']); console.log(myMap); //Map(2) {true=>7, {...}=>Array(1)} console.log([...myMap]); // (2) [Array(2), Array(2)] // 0: (2) [true, 7] // 1: (2) [{...}, Array(1)] // length: 2 // __proto__: Array(0)
-
拓展运算符-数组转化为map 结构
const map = new Map([ [true,7], [{foo:3},['abc']] ]); console.log(map); // Map(2) {true => 7, {…} => Array(1)}
-
拓展运算符-map 结构转化为对象(条件:键名为字符串)
const myMap = new Map(); myMap.set({},7) .set({},'abc'); function strMapToObj(strMap){ let obj = Object.create(null); for(let [key,val] of strMap.entries()){ obj[key] = val; } return obj; } console.log(strMapToObj(myMap)); //{true: 7, a: 'abc'}
-
拓展运算符-对象转化为map 结构
function objToStrMap(obj){ let map = new Map(); for(let key of Object.keys(obj)){ map.set(key,obj[key]); } return map } console.log(objToStrMap({true:7,no:false})); //Map(2) {'true' => 7, 'a' => 'abc'}
对象没有部署迭代器接口,为什么这里还能使用for…of呢?
这是因为Object.keys把它变成了键名构成的数组
-
15.4 set/map对比array/object
-
set对比array
let set = new Set(); let arr = new Array; //增加 set.add({t:1}); //Set(1) {{t:1}} arr.push({t:1}); //[{t:1}] //查询 set.has({t:1}); //false 因为不是同一个指针 arr.find(item => item.t); //{t: 1} //修改 set.forEach(item => item.t ? item.t = 2:""); //Set(1) {{t:2}} arr.forEach(item => item.t ? item.t = 2:""); //[{t:2}] //删除 set.forEach(item => item.t ? set.delete(item):''); //Set(0) {size: 0} let index = arr.findIndex(item => item.t); arr.splice(index,1); //[]
-
map对比array
let map = new Map(); let arr = new Array(); //增加 map.set('t',1); //Map(1) {'t' => 1} arr.push({'t':1}); //[{t,1}] //查询 map.has('t'); //true arr.find(item => item.t); //{t: 1} //修改 map.set('t',2); //Map(1) {'t' => 2} arr.forEach(item => item.t ? item.t = 2 : ''); //[{t,2}] //删除 map.delete('t'); //Map(0) {size: 0} let index = arr.findIndex(item => item.t); arr.splice(index,1); //[]
-
map/set对比object
let map = new Map(); let set = new Set(); let item = {t:1} let obj= {} //增加 map.set('t',1); //Map(1) {'t' => 1} set.add(item); //Set(1) {{t:1}} obj['t'] = 1; //{t:1} //查询 map.has('t'); //true set.has(item); //true 't' in obj; //true obj.hasOwnProperty('t') //true //修改 map.set('t',2); //Map(1) {'t' => 2} item.t = 2; //Set(1) {{t:2}} obj['t'] = 2; //{t:2} //删除 map.delete('t'); //Map(0) {size: 0} set.delete(item); //Set(0) {size: 0} delete obj['t']; //{}
15.5 总结
map/set都有迭代器接口,底层优化比array/object做得更好,结构操作比array/object更优雅,若对数据结构要求比较高,且保证数据唯一性的话就用map/set而不用array/object
16. WeakMap与WeakSet、proxy与reflect
16.1 WeakMap/WeakSet
WeakMap/WeakSet与map/set区别:
-
和map/set基本完全一致,可以理解成阉割版的map/set,为什么是阉割?看一下其原型:
-
weakMap原型
WeakMap {} [[Entries]] No properties [[Prototype]]: WeakMap constructor: ƒ WeakMap() delete: ƒ delete() get: ƒ () has: ƒ has() set: ƒ () Symbol(Symbol.toStringTag): "WeakMap" [[Prototype]]: Object
-
weakSet原型
WeakSet {} [[Entries]] No properties [[Prototype]]: WeakSet add: ƒ add() constructor: ƒ WeakSet() delete: ƒ delete() has: ƒ has() Symbol(Symbol.toStringTag): "WeakSet" [[Prototype]]: Object
发现map/set上的遍历方法全都没有了,所以说是阉割版
-
-
存储的成员只能是对象
let ws = new WeakSet(); ws.add(1); // 报错 ws.add({'t':1}); // WeakSet {{t:1}} let wm = new WeakMap(); wm.set('t',1); // 报错 wm.set({'t':1},1); // WeakMap {{…} => 1}
-
回收机制不一样,它们属于弱引用,JS垃圾回收不会考虑它们的引用
不能遍历,是因为成员都是弱引用,随时可能消失,遍历不能保证成员的存在,可能刚刚遍历结束,成员就取不到了。
16.2 proxy与reflect
-
proxy
代理,即对操作符进行拦截处理
let star = { name :'li**', age:'25', phone:'star 128888888888' } let agent = new Proxy(star,{ // 拦截读取操作 get:function(target,key){ if(key === 'phone'){ return 'agent: 1281234567' } if(key === 'price'){ return 12000 } return target[key] }, // 拦截赋值操作 set:function(target,key,value){ if(value < 10000){ throw new Error('价格太低') }else{ target[key] = value; return true; //象征性操作,说明赋值成功 } }, // 拦截 in 操作符 has:function(target,key){ console.log('请联系agent') if(key === 'customPrice'){ return target[key] }else{ return false; } }, // 拦截 delete 操作 deleteProperty: function(target, key){ if (key.indexof('_') === 0){ delete target [key]; return false; } }, // 拦截 getOwnPropertyNames、getOwnPropertySymbols、keys 操作 ownKeys: function(target) { console.log(1); console.log(target); return []; // 不返回数组会报错 }, // ... 还可以拦截其它,Reflect上有的方法都可在这里配置 }) console.log(agent.phone)//agent: 1281234567 console.log(agent.price)//12000 console.log(agent.name)//li** console.log(agent.age)//25 agent.customPrice = 1500000; //1500000 console.log(agent.customPrice); //1500000 console.log('customPrice' in agent); //请联系agent false //has是没有办法拦截for in的 for(let key in agent) { console.log(agent[key]); } console.log(delete agent.name); //false 拦截了删除 console.log(Object.getOwnPropertyNames(agent)); //1 {name: 'li**', age: '25', phone: 'star 128888888888'} []
其他代理操作:
get(target, propKey, receiver)
:拦截对象属性的读取,比如 proxy.foo 和 proxy[‘foo’]set(target.propKey, value, receiver)
:拦截对象属性的设置,比如 proxy.foo=v 或 proxy[‘foo’]=v,返回你赋的值has(target, propKey)
:拦截 propKey in proxy的操作,返回一个布尔值deleteProperty(target, propKey)
:delete proxy[propKey]的操作,返回一个布尔值ownKeys(target)
:Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、 Object.keys(proxy),返回一个数组,该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性- getOwnPropertyDescriptor(target, propKey):拦截 Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
defineProperty(target, propKey, propDesc)
:拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值preventExtensions(target)
:拦截 Object.preventExtensions(proxy),返回一个布尔值- getPrototypeOf(target):拦截 Object.getPrototypeOf(proxy),返回一个对象
isExtensible(target)
:拦截 Object.isExtensible(proxy),返回一个布尔值setPrototypeOf(target, proto)
:拦截 Object.setPrototypeOf(proxy, proto),返回一个布尔值,如果目标对象是函数,那么还有两种额外操作可以拦截。apply(target, object, args)
:拦截 Proxy 实例作为函数调用的操作,比如 proxy(…args)、 proxy.call(object,…args)、proxy.apply(…)construct(target, args)
:拦截 Proxy 实例作为构造a数调用的操作,比如 new proxy(…args)
-
reflect
把一系列的操作符变成了一种函数的行为,和函数调用没什么区别
Reflect {defineProperty: ƒ, deleteProperty: ƒ, apply: ƒ, construct: ƒ, get: ƒ, …} apply: ƒ apply() construct: ƒ construct() defineMetadata: ƒ defineMetadata(metadataKey, metadataValue, target, targetKey) defineProperty: ƒ defineProperty() deleteMetadata: ƒ deleteMetadata(metadataKey, target /* , targetKey */) deleteProperty: ƒ deleteProperty() enumerate: ƒ enumerate(target) get: ƒ () getMetadata: ƒ getMetadata(metadataKey, target /* , targetKey */) getMetadataKeys: ƒ getMetadataKeys(target /* , targetKey */) getOwnMetadata: ƒ getOwnMetadata(metadataKey, target /* , targetKey */) getOwnMetadataKeys: ƒ getOwnMetadataKeys(target /* , targetKey */) getOwnPropertyDescriptor: ƒ getOwnPropertyDescriptor() getPrototypeOf: ƒ getPrototypeOf() has: ƒ has() hasMetadata: ƒ hasMetadata(metadataKey, target /* , targetKey */) hasOwnMetadata: ƒ hasOwnMetadata(metadataKey, target /* , targetKey */) isExtensible: ƒ isExtensible() metadata: ƒ metadata(metadataKey, metadataValue) ownKeys: ƒ ownKeys() preventExtensions: ƒ preventExtensions() set: ƒ () setPrototypeOf: ƒ setPrototypeOf() Symbol(Symbol.toStringTag): "Reflect" [[Prototype]]: Object
var obj = { a: 1 }; Reflect.get(obj,'a'); // 1 Reflect.set(obj,'b',10); // true Reflect.has(obj,'a'); // true // 对比 Object.defineProperty(target,property,atrributes); // 配置错误会报错 Reflect.defineProperty(target,property,atrributes); // 配置错误返回false
17. class类、extends继承、super、class源码、修饰器模式
17.1 原型回顾
function Person(name = 'zhangsan', age = '18') {
this.name = name;
this.age = age;
}
Person.prototype.say = function() {
console.log(`my name is ${this.name}, my age is ${this.age}`);
}
var person = new Person('lisi', '19');
console.log(person.say); //my name is lisi, my age is 19
console.log(Object.getPrototypeOf(person)); //{say: ƒ, constructor: ƒ}
console.log(Object.getPrototypeOf(person).constructor === Person); //true
console.log(Person.prototype === Object.getPrototypeOf(person)); //true
17.2 class类
-
class概念
JS没有真正的类,只有纯面向对象语言(C++,Java)才有类的概念,而JS的类是在模拟构造函数,本质上是语法糖语法糖:新的语法,把原来的写法换了种方式,让代码的可读性,可维护性进一步加强
-
class定义
class Person{ constructor(name = "zhangsan",age = "18"){ //实例化的属性配置:私有属性 this.name = name; this.age = age; } //公有属性 say(){ log(`my name is ${this.name}`) } eat(){ console.log('I can eat') } drink(){ console.log('I can drink') } } console.log(new Person()); // Person {name: 'zhangsan', age: '18'} // age: "18" // name: "zhangsan" // [[Prototype]]: Object // constructor: class Person // drink: ƒ drink() // eat: ƒ eat() // say: ƒ say() // [[Prototype]]: Object new Person().eat();//My name is zhangsan new Person().eat();//I can eat new Person().drink();//I can drink
-
与传统构造函数的区别:
-
公有属性不可枚举
- 传统定义的原型可以枚举
function Person(name = 'zhangsan', age = '18') { this.name = name; this.age = age; } Person.prototype.say = function() { console.log(`my name is ${this.name}, my age is ${this.age}`); } Object.assign(Person.prototype,{ eat: function(){ console.log('I can eat') }, drink:function(){ console.log('I can drink') } }) console.log(Object.keys(Person.prototype))//["say","eat","drink"]
- class定义的原型不可枚举
class Person { constructor(name = "zhangsan", age = "18") { this.name = name; this.age = age; } say() { log(`my name is ${this.name}`) } eat() { console.log('I can eat') } drink() { console.log('I can drink') } } console.log(Object.keys(Person.prototype)) //[]
- 传统定义的原型可以枚举
-
没有指定构造器不会报错,会默认给你添加一个构造器
class Person {} console.log(new Person()); //Person {} //[[Prototype]]: Object //constructor: class Person << //[[Prototype]]: Object
-
可以更改构造器this指向
class Person { constructor() { return Object.create(null); // 默认返回this实例对象 } } console.log(new Person() instanceof Person); // false 因为没有原型,找不到构造器,所以无法比较
-
可以用函数表达式声明
let Pserson = class{ say(){ console.log(1); } } new Person.say(); // 1
-
必须通过 new 的方式执行 class(表达式方式)
立即执行会报错:
let pserson = class{ say(){ console.log(1); } }(); // 报错 person.say(); let pserson = new class{ say(){ console.log(1); } }(); person.say(); // 1 let pserson = new class{ constructor(name='zhangsan',age='18'){ this.name = name; this.age = age; } say(){ console.log(1); } }('list','19'); Person.say(); // {name: 'list', age: '19'}
-
没有函数声明提升(暂时性死区TDZ)
console.log(new Person()); //报错 class Person{}
-
不存在共有属性,只有共有方法
class Person{ a = 1; // 实际上定义的是私有属性 //constructor(){ // this.a = 1; //} say(){} } console.log(new Person()); //Person {a: 1}
私有属性存放属性是因为需要根据配置改变,共有属性存放方法是因为方法不会变
-
公有属性的私有化
即不让外界访问:
-
通过Symbol实现
const eat = Symbol(); class Person{ constructor(name,age){ this.age = age; this.name = name; } say(){ console.log(1); } [eat](){ console.log(2) } } console.log(new Person().say());//1 console.log(new Person().eat())//报错
-
从外部定义方法
class Person{ constructor(name,age){ this.age = age; this.name = name; } say(baz){ children.call(this,baz) } } function children(baz){ return this.bar = baz } // 外部无法拿到children方法
-
-
static静态属性
在类上面定义属性或方法而不是在类实例,直接通过类调用而不是类实例,类似于
function.attrs
定义的属性class Person{ static a(){ console.log(1) } } console.log(Person.a); //1
-
可以直接定义取值函数和存值函数
class Person{ get a(){ console.log(1) } set b(value){ console.log(2) } } var person = new Person(); person.a; //1 person.b = 3; //2
-
类中默认开启的严格模式
class Person { // "use strict" 默认开启 }
-
-
总结
- 属性
- 私有属性——定义对象属性
- 共有属性——定义原型属性(私有化:symbol、函数包装)
- 静态属性——类上定义属性方法
- 语法
- class 与 let 一样(TDZ)
- 共有属性方法不可枚举(以前方法可枚举)
- 默认严格模式
- 类中会有一个默认的constructor,没有不会报错;
- 必须通过 new 方式来执行
- 属性
17.3 extends继承
-
基本写法
class Parent{ constructor(name = 'zhangsan'){ this.name = name; } } class Child extends Parent{ } console.log(new Child()); //Child {name: 'zhangsan'}
-
不能继承父类静态属性
class Parent{ constructor(name = 'zhangsan'){ this.name = name; } say(){ console.log(1); } static a (){ console.log(2) } } // 派生类 class Child extends Parent{ } console.log(new Child().say()); //1 console.log(new Child().a()); //报错
-
super
以函数执行的方式将派生类this指向为为父类,并可以传参
-
使用this前需要在constructor里面执行super指向父类,否则会报错
class Parent { constructor(name = 'zhangsan') { this.name = name; } say() { console.log(1) } } class Child extends Parent { constructor(name = 'lisi', age = '19') { super(name); // 这里的super做了两件事:1. 修改this为父类实例 this.type = 'child'; // 2. 通过super传参给父类 this.age = age; } } console.log(new Child()); //Child {name: 'lisi', type: 'child', age: '19'}
-
注意点:
-
必须在constructor内部才能使用
-
只有super在最前面的时候才能使用this
因为派生类的this是父类继承过来的,所以在调用this之前一定要先执行super
-
-
其他用法
-
在对象当中指代对象的原型
let proto = { y: 20, Z: 40 } let obj = { x: 10, foo() { console.log(super.y) } } Object.setPrototypeOf(obj, proto); obj.foo(); //20
-
在静态方法中指向自己的父类(了解)
-
-
17.4 class源码
-
特征:
- TDZ
- use strict
- 不可枚举
- 必须new执行
- 不写constructor不会报错
-
源码分析:
class Person { constructor(name = "zhangsan", age = "18") { this.name = name; this.age = age; } say() { console.log(' hello world'); } drink() { console.log('drink'); } static eat() { console.log('eat'); } }
使用babel编译看一下结果:
// 默认严格模式 "use strict"; function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } } // 判断是否new执行 function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } // 处理共有属性、私有属性 function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } var Person = /*#__PURE__*/ function() { function Person() { // 参数默认值 var name = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "zhangsan"; var age = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "18"; // 判断是否用new执行 _classCallCheck(this, Person); // 处理私有属性 this.name = name; this.age = age; } // 处理共有属性、静态属性 _createClass(Person, [{ key: "say", value: function say() { console.log(' hello world'); } }, { key: "drink", value: function drink() { console.log('drink'); } }], [{ key: "eat", value: function eat() { console.log('eat'); } }]); return Person; }();
17.5 修饰器模式
-
修饰器模式概念:
为对象添加新的功能,而不改变原有的结构和功能;
-
基本形式:
class Person { constructor(name = "zhangsan", age = "18") { this.name = name; this.age = age; } @readonly //修饰了下面的属性 say() { console.log('hello world'); } static eat() { console.log('eat'); } static eat1() { console.log('eat'); } }
-
babel配置:
-
安装包(规则集,脚手架,es7编译插件)
npm i babel-preset-env npm i babel-cli --save-dev npm i babel-plugin-transform-decorators-legacy --save-dev --registry=https://registry.npmmirror.com
实时监听
npx babel test.js --watch --out-file bundle.js
-
配置
package.json:{ ... "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "babel test.js -o bundle.js" // babel输出文件 }, ... }
babelrc:
{ "presets": ["env"], // 所有规则集 "plugins": ["babel-plugin-transform-decorators-legacy"] //es7编译插件 }
-
-
浏览器不支持修饰器(es7),需要用babel编译
-
修饰类
只有一个参数,参数是类的构造器
@testable // 修饰下面的类 class Person { constructor(name = "zhangsan", age = "18") { this.name = name; this.age = age; } say() { console.log('hello world'); } eat() { console.log('eat'); } } let person = new Person(); console.log(person); function testable(target) { console.log(target); // f Person() {...} }
-
修饰属性
有三个参数,参数依次是:类的实例,属性名,属性描述符
// 【类的定义】 class Person { constructor(name = "zhangsan", age = "18") { this.name = name; this.age = age; } @readonly //修饰下面属性 say() { console.log('hello world'); } eat() { console.log('eat'); } } // 【逻辑代码】 作用:实现了业务和逻辑的分离 function readonly(target, name, descriptor) { console.log(target, name, descriptor); // {constructor: ƒ, say: ƒ, eat: ƒ} 'say' {writable: true, enumerable: false, configurable: true, value: ƒ} descriptor.writable = false; } let person = new Person(); // 【操作语句】 // 访问 person.say(); // hello world // 写入 person.say = function() { // 报错 console.log(1); }
-
-
使用场景:埋点分析
埋点即埋伏的点,每做一次数据操作,会有对应的数据来记录当前的操作,然后输出出来,然后根据代码的执行分析当前的行为(有点像控制台日志)
// 【拓展的功能】 优势:与原本的功能相互独立,可直接复用 // 接收修饰器的参数 let log = (type) => { return function(target, name, descriptor) { let src_method = descriptor.value; // 重写value 收集方法的参数(严谨) descriptor.value = (...arg) => { // this指向类 src_method.apply(target, arg); // 增加日志逻辑 console.log(type); } } } // 【逻辑代码】 class AD { @log('show') show() { console.log('ad is show'); } @log('click') click() { console.log('ad is click'); } } let ad = new AD(); ad.show(1,2,3); ad.click(); // 输出: // ad is show // show // ad is click // click
设计原则:遵循了开放封闭原则:对拓展开放,对修改封闭,简而言之就是不修改原来的代码去拓展功能
18. 异步的开端(JS执行机制、异步存在的问题、promise演变历史)、promise初识(三种状态(padding/resolve/reject)、executor执行者、then回调绑定、执行顺序、链式调用)
18.1 异步的开端
-
JS执行机制
JS是单线程的语言,所以会有代码阻塞,要想避免代码阻塞就得通过异步,而异步的实现原理就是通过事件环,它会把异步任务寄托到浏览器,浏览器将异步任务处理完后会将其推送到任务队列,等JS主线程执行完毕后,会一个一个执行任务队列里的异步任务。
-
异步存在的问题
-
回调地狱 —— 难于维护,不便拓展
呈现倒三角的写法就是回调地狱
let fs = require('fs'); fs.readFile('./name.txt','utf-8',(err,data)=>{ console.log(data); // ./number.txt if(data){ fs.readFile(data,'utf-8',(err,data)=>{ console.log(data); // score.txt fs.readFile(data,'utf-8',(err,data)=>{ console.log(data); // 90 }) }) } })
-
try-catch —— 难以捕获异常
只能捕获同步异常,无法捕获异步错误
直接报错
try { setTimeout(()=>{ console.log(a); // Uncaught ReferenceError: a is not defined },90) } catch (e) { console.log(e); }
捕获不到
let fs = require('fs'); try { fs.readFile('./nam.txt', 'utf-8', (err, data) => { if (err) { console.log(err); // [Error: ENOENT: no such file or directory, open 'C:\Users\21757\Desktop\ES6\nam.txt'] { // errno: -4058, // code: 'ENOENT', // syscall: 'open', // path: 'C:\\Users\\21757\\Desktop\\ES6\\nam.txt' // } } }); } catch (e) { console.log(e); }
-
同步并发异步任务 —— 没法判断谁先执行
每一个异步函数中都要加入 arr.length === 3 && show(arr) 判断是否读取完
let fs = require('fs'); let arr = [] function show(data){ console.log(data); } fs.readFile('./name.txt','utf-8',(err,data)=>{ if(data){ arr.push(data) } arr.length === 3 && show(arr); }) fs.readFile('./number.txt','utf-8',(err,data)=>{ if(data){ arr.push(data) } arr.length === 3 && show(arr); }) fs.readFile('./score.txt','utf-8',(err,data)=>{ if(data){ arr.push(data) } arr.length === 3 && show(arr); }) // ['./number.txt','scores.txt','90']
-
-
promise演变历史
-
Callbacks() —— 管理回调列表(已过时)
在jquery中可以通过 add 方法将异步任务推到 Callbacks 里面进行管理
// 引入JQuery let cb = $.Callbacks(); function a(x,y){ console.log('a',x,y) } function b(x,y){ console.log('b',x,y) } cb.add(a,b); //添加异步方法 cb.fire(10,20); //执行所有异步 //a 10 20 //b 10 20
-
deferred(即将淘汰)
底层是通过 Callbacks 管理当前的回调,实现异步的管理
缺点:可以显示调用 reject,于是采用 promise 来解决,这就是 promise 的由来
function waitHandle(){ var dtd = $.Deferred(); //创建一个 deferred 对象; var wait = function(dtd){ //传入一个 deferred 的对象 var task = function(){ console.log('执行完成'); dtd.resolve(); //表示异步任务已经完成 // dtd.reject(); //表示异步任务失败或出错;打印失败的回调 } setTimeout(task,2000); // return dtd; //里层函数,返回 deferred 对象 return dtd.promise(); //注意,这里返回的是 promise 而不是直接返回 deferred } // 外层函数返回 里层函数的执行结果 return wait(dtd) } var w = waitHandle(); console.log(w); //w.done(function(){ // console.log('ok'); //}).fail(function(){ // console.log('err'); //}); // 执行完成 ok // w.reject(); // deferred弊端:deferred 对象可以显示调用 reject 方法,而 promise 对象无法调用 w.then(function(){ console.log('ok'); }).fail(function(){ console.log('err'); }); // 执行完成 ok
-
promise A+规范
本身是社区的一个规范,用于定义 promise 的用法,最后被囊括在了es6
-
18.2 promise
-
promise初识
是一种异步操作,js内置的构造函数
参数是一个函数(executor 执行者)
console.log(new Promise(function(resolve,reject){})); // Promise {<pending>} // [[Prototype]]: Promise // catch: ƒ catch() // constructor: ƒ Promise() // finally: ƒ finally() // then: ƒ then() // Symbol(Symbol.toStringTag): "Promise" // [[Prototype]]: Object // [[PromiseState]]: "pending" // [[PromiseResult]]: undefined
-
同步执行
new Promise(function(resolve,reject){ console.log('promise'); }); console.log(1); // 'promise' 1
-
三种状态
- panding(进行中)
- fufilled(也叫resolve)(已成功)
- reject(已失效)
两个过程:padding - resolve、padding - reject,promise的resolve、reject代表的是这两个过程
-
两个特征
- 对象的状态不受外界影响(唯一能影响的就是当前它所代表的异步事件)
- 状态不可逆(promise固化以后,再对promise对象添加回调,是可以直接拿到这个结果的,如果说是事件的话,一旦错过了,就是真的错过了)
-
executor参数
通过调用resolve、reject方法,改变当前的异步操作的状态,并调用绑定回调函数
-
绑定回调函数
当 promise 的状态发生变化时调用
通过 then 方法绑定状态为成功或者失败的回调函数
参数:注册成功回调 / 注册失败回调
let promise = new Promise((resolve,reject)=>{ setInterval(function(){ Math.random()*100 >60 ? resolve('ok'):reject('no'); },30); }) promise.then((val)=>{ console.log(val); },(reason)=>{ console.log(reason); }) //随机的ok no
通过promise代表当前的异步任务,当异步任务完成以后调用相应的方法来改变promise当前的状态,并调用promise通过.then的方式绑定的回调函数
-
执行顺序
promise里面是同步执行
而resolve、reject是promise的异步操作,执行会改变当前promise的状态,调用绑定的回调函数
let promise = new Promise((resolve,reject)=>{ console.log(0); resolve('1'); // 异步操作,调用的回调属于异步任务 }) promise.then((val)=>{ console.log(val); },(reason)=>{ console.log(reason); }) console.log(2); // 先执行主线程任务 0 2 再执行任务队列当中的 1
当有两个异步任务时
setTimeout(function(){ console.log('SET TIME'); },30); let promise = new Promise((resolve,reject)=>{ console.log(0); resolve('1'); }); promise.then((val)=>{ console.log(val); },(reason)=>{ console.log(reason); }); console.log(2); //0 2 1 SET TIME
JS异步代码中,
分为宏任务(宏任务队列),
微任务(微任务队列):promise、process.nextTick() 优先级更高(除了这两个其它都是宏任务)
Promise.resolve().then(()=>{ //在executor里面调用是等效的 console.log('promise1'); setTimeout(()=>{ console.log('setTime2'); }) }) setTimeout(()=>{ console.log('setTime1'); Promise.resolve().then(()=>{ console.log('promise2'); }) }) //第一轮:promise1 setTime1 第二轮:promise2 setTime2
-
链式调用
then默认返回promise对象,所以可以链式调用
let promise = new Promise(function(resolve,reject){ setInterval(function(){ Math.random * 100 > 60 ? resolve('ok') : reject('no'); },30) }) promise.then((val)=>{ console.log(val) },(reason)=>{ console.log(reason) }).then((val)=>{ // 因为上一个默认promise没有传值,所以这里val是undefined console.log(val); },(reason)=>{ console.log(reason); }) //no undefined
第一次then的返回值作为下一次then的执行参数
let promise = new Promise(function(resolve,reject){ setInterval(function(){ Math.random() * 100 > 60 ? resolve('ok') : reject('no'); },30) }); promise.then((val)=>{ console.log(val) return 3; },(reason)=>{ console.log(reason) return 2; }).then((val)=>{ console.log(val); },(reason)=>{ console.log(reason); }); // ok 3 or no 2
如果第一次就return一个新的Promise 那么就会调用第二个then,参数就自动获取resolve中的了
let promise = new Promise(function(resolve,reject){ setInterval(function(){ Math.random() * 100 > 60 ? resolve('ok') : reject('no'); },30) }) promise.then((val)=>{ console.log(val) return new Promise((resolve,reject)=>{ resolve('newPromise') }) },(reason)=>{ console.log(reason) return new Promise((resolve,reject)=>{ reject('rejectPromise') }) }).then((val)=>{ console.log(val); },(reason)=>{ console.log(reason); }) //ok newPromise or no rejectPromise
19. promise深入(then简写、catch方法、promise固化、状态依赖、异步管理方法(all/race)、thenable、构造器上的resolve/reject、函数Promise化)、promisify函数封装
19.1 promise深入
-
若在executor里面抛出错误,promise状态会直接更改为reject
let promise = new Promise(function(resolve,reject){ resolve(a); // 还没来得及执行就抛出了错误,将状态更改为reject,然后触发回调 }) promise.then((val)=>{ console.log('resolve: '+val) },(reason)=>{ console.log('reject: '+reason) }) // reject: a is not defined
-
then的简写
不绑定成功回调用null占位,不绑定失败回调可以省略
// 失败回调可省略 promise.then((value)=>{ console.log('resolve: '+value) }); // 成功回调用null占位 promise.then(null,(reason)=>{ console.log('reject: '+reason) }); //等同于 promise.catch(function(reason){ console.log('reject: '+reason) })
-
catch
-
基本使用
也可以捕获异常
let promise = new Promise(function(resolve,reject){ resolve(a); }) promise.catch(function(reason){ console.log(reason) }); // ReferenceError: a is not defined
-
语义化写法
当需要捕获异常的时候,推荐使用第二种写法
promise.then(function(){ },function(){ }); // 推荐 promise .then(function(){ }) catch(function(){ })
-
特性
- 冒泡的特性
- 状态固化之后,无法捕获异常
- 链式调用 then() 的时候,如果什么参数都不传会被直接忽略
let promise = new Promise(function(resolve,reject){ resolve(a); }) promise.then(function(val){ console.log(val); }).then().then().catch(function(err){ console.log(err); }) // ReferenceError: a is not defined
-
-
promise固化
如果promise状态发生变化,就意味着状态不会再发生改变
let promise = new Promise(function(resolve,reject){ resolve('ok'); console.log(a); // 这里也执行了,但由于状态已经固化了,所以catch捕获不到 }) promise.then((val)=>{ console.log(val); }).catch((err)=>{ console.log(err); }); //ok
-
状态依赖
每一个promise代表的是一个异步操作,每个异步操作都有相应的状态,那么在有多个promise嵌套的情况下,状态又是怎样传递的呢?
当executor的resolve、reject的参数是另一个promise的时候,那么当前promise的状态,就取决于另一个promise的状态
let p1 = new Promise((resolve,reject)=>{ //也是同步执行 setTimeout(function(){ reject(new Error('fail')); },3000); }); let p2 = new Promise((resolve,reject)=>{ setTimeout(function(){ resolve(p1); },1000); }); p2.then(result=>console.log(result)) .catch(err=>console.log(err)); //Error: fail
当前p2的状态依赖于p1,就会导致p2自己的状态无效,它的状态取决于p1的状态
-
resolve/reject不会终止executor执行
const p1 = new Promise((resolve,reject)=>{ //resolve(1); reject(new Error()); //微任务 console.log(2); }); p1.then(res=>console.log(res)) .catch(err=>console.log(err)); console.log(3); //2 3 Error
-
race/all管理异步的关系
promise通过race、all管理异步之间的关系
-
all
批量处理多个异步操作,返回值是promise对象
参数是具有iterator接口的对象(数组),里面是相应的promise
只有所有promise状态为成功情况下,才会触发成功回调
成功回调参数是一个数组,其顺序与iterable里promise顺序一致
const fs = require('fs'); let promise1 = new Promise((resolve,reject)=>{ fs.readFile('./name.txt','utf-8',function(err,data){ if(err){ console.log(err); } resolve(data); }); }); let promise2 = new Promise((resolve,reject)=>{ fs.readFile('./number.txt','utf-8',function(err,data){ if(err){ console.log(err); } resolve(data); }); }); let promise3 = new Promise((resolve,reject)=>{ fs.readFile('./score.txt','utf-8',function(err,data){ if(err){ console.log(err); } resolve(data); }); }); const p = Promise.all([promise1,promise2,promise3]); //返回值是promise对象 p.then(res=>console.log(res)); //[ './number.txt', 'score.txt', '90' ]
只要有一个promise状态为失败,就会触发失败回调
如果全部promise状态都为失败,则只触发iterable里第一个promise状态为失败的回调,其余直接忽略
let promise1 = new Promise((resolve,reject)=>{ setTimeout(function(){ reject('promise1: 1000ms') },1000) }) let promise2 = new Promise((resolve,reject)=>{ setTimeout(function(){ reject('promise2: 2000ms') },2000) }) let promise3 = new Promise((resolve,reject)=>{ setTimeout(function(){ reject('promise3: 3000ms') },3000) }) let p = Promise.all([promise1,promise2,promise3]); p.then(res=>console.log(res) ).catch((error)=>{ console.log(error); }) //失败回调:promise1:1000ms 后面直接忽略
-
race
批量处理多个异步操作,返回值是promise对象
任意一个promise状态为成功或失败,就会触发相应的回调,其余直接忽略
let promise1 = new Promise((resolve,reject)=>{ setTimeout(function(){ //resolve('promise1: 1000ms'); reject('promise1: 1000ms'); },1000) }) let promise2 = new Promise((resolve,reject)=>{ setTimeout(function(){ //resolve('promise2: 2000ms'); reject('promise2: 2000ms'); },2000) }) let promise3 = new Promise((resolve,reject)=>{ setTimeout(function(){ //resolve('promise3: 3000ms'); reject('promise3: 3000ms'); },3000) }); let p = Promise.race([promise1,promise2,promise3]); p.then(res=>console.log(res) ).catch((error)=>{ console.log(error); }); //promise1:1000ms 不管成功或失败都返回这个,因为它最先完成 其余直接忽略
-
-
构造器上的resolve/reject
-
thenable
绑定了then方法的对象就是thenable,用于转换成promise对象
-
resolve —— 转换成功状态的promise对象
返回一个promise对象,当前promise的状态取决于resolve的参数
-
是一个thenable
let obj = { then:function(resolve,reject){ resolve(42); } } let p1 = Promise.resolve(obj); p1.then(function(value){ console.log(value); }); //42
-
字符串
let p1 = Promise.resolve('hello'); p1.then(res=>{ console.log(res) }); //hello
-
没有参数
let p1 = Promise.resolve(); p1.then(res=>{ console.log(res) }); //undefined
-
-
reject —— 转换失败状态的promise对象
返回一个promise对象,当前promise的状态取决于resolve的参数
-
thenable(不要用)
let obj = { then: function(resolve, reject) { reject(new Error('hello')); } } let p1 = Promise.reject(obj); p1.then(null, function(value) { console.log(value); }); //{ then: [Function: then] } what the hell? 所以不要用
-
字符串
let p1 = Promise.reject('hello'); p1.then(null,res=>{ console.log(res) }); //hello
-
不传参
let p1 = Promise.reject(); p1.then(null,res=>{ console.log(res) }); //undefined
-
-
-
函数Promise化
将异步函数用promise包装一遍
const fs = require('fs'); function readFile(path){ return new Promise((resolve,reject)=>{ fs.readFile(path,'utf-8',(err,data)=>{ if(data){ resolve(data); } }) }) } readFile('./name.txt') .then(data => readFile(data)) .then(data => readFile(data)) .then(data => console.log(data)); //99
19.2 promisify
-
promisify概念
能够将所有函数promise化的方法
-
promisify函数封装(node)
const fs = require('fs'); function promisify(func){ return function(...arg){ // 1.返回promise化的方法,传入需promise化的方法 return new Promise((resolve,reject)=>{ // 2.调用promise化的方法,需传入相应参数 func(...arg,(err,data)=>{ // 3.根据参入的方法和参数,执行相应的方法 if(err){ reject(err); }else{ resolve(data) } }) }) } } let readFile = promisify(fs.readFile); // 第一步:传入需promise化的方法 readFile('./name.txt','utf-8') // 第二步:需传入相应参数 .then(data=>readFile(data,'utf-8')) .then(data=>readFile(data,'utf-8')) .then(data=>console.log(data)); //99
-
node工具函数中也提供了promisify
const util = require('util'); let readFile = util.promisify(fs.readFile); // 也能实现 readFile('./name.txt','utf-8') .then(data=>readFile(data,'utf-8')) .then(data=>readFile(data,'utf-8')) .then(data=>console.log(data)); //99
-
优化node提供的promisify
把fs上的所有方法转成promisify方法
const fs = require('fs'); const util = require('util'); let readFile = util.promisify(fs.readFile); let readdir = util.promisify(fs.readdir); let writeFile = util.promisify(fs.writeFile); //每次使用前都要传入promise化的方法,比较麻烦,本次优化就是解决这个问题 function promisify(func){ return function(...arg){ return new Promise((resolve,reject)=>{ func(...arg,(err,data)=>{ if(err){ reject(err); }else{ resolve(data) } }) }) } } function promisifyAll(obj){ for(let [key,fn] of Object.entries(obj)){ // 注意:这里for...of本不能遍历obj,但这里用Object.entries把obj转成了键和值的数组,所以可以迭代 if(typeof fn === 'function' ){ obj[key+'Async'] = promisify(fn); } } } promisifyAll(fs) fs.readFileAsync('./name.txt','utf-8') .then(data=>readFileAsync(data,'utf-8')) .then(data=>readFileAsync(data,'utf-8')) .then(data=>console.log(data)); //99
20. 迭代器深入(迭代器的实现、迭代器与迭代器模式、内部迭代器和外部迭代器、部署迭代器接口、默认调用迭代器的情况、)、generator(function*fn{}、yield、状态机 *、co)
20.1 迭代器深入
-
迭代器的实现
function makeIterator(arr){ var iterRatorIndex = 0; return{ next(){ return arr.length > iterRatorIndex ? {value:arr[iterRatorIndex++],done:false}: {value:undefined,done:true} } } } var test = makeIterator(['a','b']); //闭包:这里返回对象里面的函数,绑定了外部的作用域 console.log(test.next());//{value: "a", done: false} console.log(test.next());//{value: "b", done: false} console.log(test.next());//{value: undefined, done: true}
-
迭代器与迭代器模式
-
迭代器:能够迭代有序的,连续的结构,每一次抽取一位消耗数据的形式
-
迭代器模式:结构化的模式,迭代器是结构化模式的实现方式,从源以一次一个的方式抽取
抽取就是每次只遍历一个值
-
-
内部迭代器和外部迭代器
前端不分内外迭代器
-
内部迭代器:系统内部定义好的迭代规则,在进行调用的时候就可以拿到所有遍历的元素
-
外部迭代器:自己部署定义一个迭代器接口,每一次抽取一个数据、每一次迭代一个数据
-
-
部署迭代器接口——next方法(迭代对象)
对象并不具备迭代器接口(因为对象是无序的所以不能抽取),要想迭代就得部署一个迭代器接口
-
方法一
let obj = { start:[1,3,2], end:[7,8,9], [Symbol.iterator](){ let arr = [...this.start,...this.end]; let iterRatorIndex = 0; return{ next(){ return arr.length > iterRatorIndex ? {value:arr[iterRatorIndex++],done:false}: {value:undefined,done:true} } } } } for(let a of obj){ console.log(a); }// 1 3 2 7 8 9
-
方法二
map结构则是有序的,让map模拟对象进行迭代,以键值的方式进行输出
可迭代的数据类型:array/map/set/string/typeArray/NodeList/arguments
let obj = { a:1, b:2, [Symbol.iterator]() { let map = new Map(); for (let [key, value] of Object.entries(obj)) { map.set(key, value); } let mapArr = [...map.entries()]; let iterRatorIndex = 0; return { next() { return mapArr.length > iterRatorIndex ? { value: mapArr[iterRatorIndex++],done:false}: {value:undefined,done:true} } } } } for(let a of obj){ console.log(a); } //(2) ["a", 1] ["b", 2] //抽取 var a = obj[Symbol.iterator](); console.log(a.next()); //{value: Array(2), done: false}
用for(let [key,value] of Object.entries(obj))也能解决这个问题,但是不能实现抽取,因为不具备迭代器接口
-
-
默认会调用iterator的情况
- …拓展运算符
- for…of
- Array.from()
- map/set
- Promise.all([…])/Promise.race([…])
- yield
-
部署迭代器接口——return方法(了解)
终止for循环的方法都会触发return方法(比如break/throw new Error())
let obj = { a:1, b:2, [Symbol.iterator]() { let map = new Map(); for (let [key, value] of Object.entries(obj)) { map.set(key, value); } let mapArr = [...map.entries()]; let iterRatorIndex = 0; return { next() { return mapArr.length > iterRatorIndex ? { value: mapArr[iterRatorIndex++],done:false}: {value:undefined,done:true} }, return() { console.log('触发了错误'); return {value: 1, done: false} // 不返回该对象会报错 } } } } for(var i of obj){ console.log(i); throw new Error('错误'); } //["a", 1] //触发了错误 //Uncaught Error: 错误
20.2 generator
-
概念
生成器函数,返回值是迭代器实例
-
基本形式
function * test(){} let iter = test(); console.log(iter); test {<suspended>} //[[GeneratorLocation]]: VM657:1 //[[Prototype]]: Generator //[[Prototype]]: Generator //constructor: GeneratorFunction {prototype: Generator, Symbol(Symbol.toStringTag): 'GeneratorFunction', constructor: ƒ} //next: ƒ next() << //return: ƒ return() //throw: ƒ throw() //Symbol(Symbol.toStringTag): "Generator" ...
-
yield
-
特点
-
产出迭代器对象,暂停函数运行
-
返回值默认是undefined,如果要有返回值则需通过next传参
function * test(){ console.log(1); yield 'a'; console.log(2); yield 'b'; console.log(3); let a = yield '5' console.log(a); return 'd'; yield 'c'; } let iter = test(); console.log(iter.next()); console.log(iter.next()); console.log(iter.next()); console.log(iter.next()); console.log(iter.next()); // 1 // {value: "a", done: false} // 2 // {value: "b", done: false} // 3 // undefined // {value: "d", done: true} // {value: undefined, done: true}
-
yield是一个单独的表达式,嵌套使用要加()
function * test(){ console.log('hello'+(yield 123)); } let iter = test(); console.log(iter.next()); {value: 123, done: false}
-
yield作为函数形参
function * test(){ foo(yield 'a',yield 'c') } function foo(a,b){ console.log(a,b); } let iter = test(); console.log(iter.next()); console.log(iter.next()); console.log(iter.next()); // {value: "a", done: false} // {value: "c", done: false} // undefined undefined // {value: undefined, done: true}
-
-
yield/return区别:
yield 暂停(有记忆的功能)
return 结束(没有记忆功能)
注: yield 只能出现在生成器函数中,否则会报错
-
for…of 遍历产出值
function * test(){ yield 1; yield 2; yield 3; yield 4; yield 5; } for(let i of test()){ console.log(i); } //1 2 3 4 5
-
通过next给yield传参
永远拿不到第一次的值,所以第一个参数没必要传,蛇形传值
function * test(){ let value1 = yield 1; // 蛇形传值方式 console.log('value1'+value1); let value2 = yield 2; console.log('value2'+value2); let value3 = yield 3; console.log('value3'+value3); let value4 = yield 4; console.log('value4'+value4); } let iter = test(); console.log(iter.next('none')); console.log(iter.next('two')); console.log(iter.next('three')); // {value: 1, done: false} // value1two // {value: 2, done: false} // value2three // {value: 3, done: false}
-
迭代器接口优化
因为yield得产出值就为迭代函数,所以可以直接用yield去替代以前next的写法
let obj = { start:[1,2,3], end:[4,5,6], [Symbol.iterator] : function*(){ var itemIndx = [...this.start,...this.end], length = itemIndx.length, idxof = 0; while(length>idxof){ yield itemIndx[idxof++] } } } for(var c of obj){ console.log(c); } console.log(obj[Symbol.iterator]().next()); // 1 2 3 4 5 6 {value: 1, done: false}
-
迭代器方法
-
next() —— 抽取产出值,返回值是迭代器对象
-
return() —— 终止迭代,往后迭代都是
{value:undefine,done:true}
-
throw() —— 抛出错误,由于yield会暂停,导致try-catch没执行完,所以捕获不到,只有try-catch执行完了才能捕获到错误(相当于next,也可以抽取迭代)
var g = function * (){ yield; try{ yield; }catch(e){ console.log('生成器内部异常:' + e); } console.log(1); } var i = g(); console.log(i.next()); // {value:undefined,done:false} --没执行完 console.log(i.next()); // {value:undefined,done:false} --执行完了 console.log(i.throw('a')); // 生成器内部异常:a --抛出错误 注意:这里只能throw一次,两次会报错 console.log(i.next()); // {value:undefined,done:true}
-
-
cry/catch可以捕获生成函数的异步错误
let fs = require('fs'); let util = require('util'); let co = require('co'); let readFile = util.promisify(fs.readFile); function * read(){ let value1 = yield readFile('./name.txt', 'utf-8'); let value2 = yield readFile(value1, 'utf-8'); let value3 = yield readFile(value2, 'utf-8'); return value3; } let promise = co(read()); promise.then((val)=>{ console.log(val); }); // 99
-
-
generator状态机
-
generator状态机
let fs = require('fs') function promisify(fn){ return function(...args){ return new Promise((resolve,reject)=>{ fn(...args,(err,data)=>{ if(data){ resolve(data) }else{ reject(err) } }) }) } } let readFile = promisify(fs.readFile) function * read(){ let value1 = yield readFile('./name.txt','utf-8'); let value2 = yield readFile(value1,'utf-8'); let value3 = yield readFile(value2,'utf-8'); console.log(value3); } let iter = read(); let {value,done} = iter.next(); value.then((val)=>{ let{value,done} = iter.next(val) value.then((val2)=>{ let{value,done} = iter.next(val2) value.then((value)=>{ console.log(value); }) }) }) // 99
步骤:
-
首先通过Promisify把异步函数promise化
-
然后把所有异步函数作为产出值放入生成函数,通过next进行抽取调用,返回值是迭代器对象
{value:promise,done:false}
-
再通过模式匹配拿到promise,通过then拿到数据,继续通过next进行抽取调用
-
-
co源码实现
co是一个模块,是由TJ开发(整个git最高效、高产的一个人)
function Co(iter){ return new Promise((resolve,reject)=>{ let next = (data) =>{ let {value,done} = iter.next(data); if(done){ resolve(value) }else{ value.then((val)=>{ next(val); }) } } next(); // 递归 }) } let promise = Co(read()) promise.then((val)=>{ console.log(val); }) // 99
-
co
它让异步的执行更加简单,不需要每次都调用next
它也是async的由来
npm i co -D
let co = require('co'); let promise = Co(read()) promise.then((val)=>{ console.log(val); }); // 99
-
21. async与await、all错误忽略实现
21.1 概念
async:本质上就是generator生成器函数
await:就相当于是yield产出
21.2 优势
- 内置的执行器是co —— 简化了异步的执行
- 语义化更强
- 更广的使用性 —— 后面可以不止是promise对象
- 返回值一定是promise对象(内部自动触发resolve和reject)
21.3 基本使用
const fs = require('fs');
const util = require('util');
let readFile = util.promisify(fs.readFile)
async function read() {
try { // 生成函数与普通函数不同的是,可以通过try/catch捕获异步异常
let value1 = await readFile('./name.txt', 'utf-8'); // 直接拿到异步结果
let value2 = await readFile(value1, 'utf-8');
let value3 = await readFile(value2, 'utf-8');
console.log('hello world');
return value3 // 不管返回什么,最终都会变成promise对象,并自动帮你调用resolve或reject
} catch (error) {
console.log('91: ' + error)
}
}
let promise = read(); // 返回promise对象
promise.then((val) => {
console.log(val);
});
// 正常输出:
// hello world
// 90
// 异常输出:./name.text => ./nam.txt
// 91: Error: ENOENT: no such file or directory, open 'C:\Users\21757\Desktop\ES6\nam.txt'
// undefined
21.4 实现all的错误忽略
用all方法来执行异步操作,就会直接抛出错误没法执行后面的,那么我们可以通过async和await/try-catch/set来配合实现这个功能,平常不会这么做
let fs = require('fs');
let util = require('util')
let readFile = util.promisify(fs.readFile)
async function read(){
let value1, value2, value3;
let res = new Set();
try{
value1 = await readFile('name.txt','utf-8');
}catch(err){ // 注意:这里catch不能去掉,去掉会导致无法捕获异常
}
try{
value2 = await readFile('number.tx','utf-8');
}catch(err){
}
try{
value3 = await readFile('score.txt','utf-8');
}catch(err){
}
res.add(value1)
res.add(value2)
res.add(value3)
return res;
}
read().then((val)=>{
console.log(val);
})
// Set(3) { './number.txt', undefined, '90' } 然后再把undefined去掉就可以了
22. Promise源码重写
22.1 基本功能
class MyPromise { // new的时候传入executor函数
constructor(executor) {
this.state = 'pending'; // 提供状态、成功返回值与失败返回值
this.value = undefined;
this.reason = undefined;
let resovle = (value) => { // 定义resolve函数
this.state = 'fullFilled';
this.value = value;
}
let reject = (reason) => { // 定义reject函数
this.state = 'reject';
this.reason = reason;
}
executor(resovle, reject); // 提供resolve和reject两个函数
}
then(onFullFilled, onRejected) { // 定义then方法,接收成功与失败的回调函数
if (this.state === 'fullFilled') { // 如果状态为成功,则返回成功的返回值,通过形参传给回调函数
onFullFilled(this.value);
}
if (this.state === 'rejected') { // 如果状态为失败,则返回失败的返回值,通过形参传给回调函数
onRejected(this.reason);
}
}
}
const p1 = new MyPromise((resolve, reject) => {
resolve(1);
});
p1.then((res) => {
console.log(res);
}, (err) => {
console.log(err);
});
// 输出1
22.2 处理异步任务、绑定多个回调
executor执行的时候,resolve并没有执行,1秒钟后resolve才执行
所以说在new MyPromise的时候,MyPromise的状态还是pennding,此时就可以认定是在处理异步任务
class MyPromise {
constructor(executor) {
this.state = 'pending';
this.value = undefined;
this.reason = undefined;
this.onFullFilledCallbacks = []; // 当多次绑定回调,则将其存入对应的成功/失败数组中遍历执行
this.onRejectedCallbacks = [];
let resovle = (value) => {
this.state = 'fullFilled';
this.value = value;
this.onFullFilledCallbacks.forEach(fn => fn()); // 这里可以传参fn(this.value),但是并不好,应该在then中统一处理参数
}
let reject = (reason) => {
this.state = 'reject';
this.reason = reason;
this.onRejectedCallbacks.forEach(fn => fn()); // fn(this.reason)
}
executor(resovle, reject);
}
then(onFullFilled, onRejected) {
// 同步情况
if (this.state === 'fullFilled') {
onFullFilled(this.value);
}
if (this.state === 'rejected') {
onRejected(this.reason);
}
// 异步情况
if (this.state === 'pending') {
// 用函数包装一次,有点像代理模式
this.onFullFilledCallbacks.push(() => {
onFullFilled(this.value);
});
this.onRejectedCallbacks.push(() => {
onRejected(this.reason);
});
}
}
}
const p1 = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve(1); // executor执行的时候,resolve并没有执行,1秒钟后resolve才执行
}, 1000)
});
p1.then((res) => {
console.log(res);
}, (err) => {
console.log(err);
});
p1.then((res) => {
console.log(res);
}, (err) => {
console.log(err);
});
// 输出1 1
22.3 链式调用、问题检测工具
-
链式调用
链式操作不能返回this,因为this是上一次的promise,而我们需要返回的是一个全新的promise
const isFunction = (value) => typeof value === 'function'; class MyPromise { constructor(executor) { this.state = 'pending'; // 初始状态:fulfilled,rejected this.value = undefined; // 成功的返回值 this.reason = undefined; // 失败的返回值 this.onFullFilledCallbacks = []; this.onRejectedCallbacks = []; let resovle = (value) => { this.state = 'fullFilled'; this.value = value; this.onFullFilledCallbacks.forEach(fn => fn()); } let reject = (reason) => { this.state = 'rejected'; this.reason = reason; this.onRejectedCallbacks.forEach(fn => fn()); } try { executor(resovle, reject); } catch (err) { reject(err); } } then(onFullFilled, onRejected) { onFullFilled = isFunction(onFullFilled) ? onFullFilled : data => data; onRejected = isFunction(onRejected) ? onRejected : err => { throw err; } // 每一次调用then, 都会有一个p2 const p2 = new MyPromise((resolve, reject) => { // 每当new MyPromise的时候,里面的代码是同步执行的 let x; // 同步情况 if (this.state === 'fullFilled') { setTimeout(() => { try { x = onFullFilled(this.value); // resolve(x); // 判断返回值是否为promise,然后作相应的处理 resolvePromise(p2, x, resolve, reject); } catch (err) { reject(err); } }, 0); } if (this.state === 'rejected') { setTimeout(() => { try { x = onRejected(this.reason); resolvePromise(p2, x, resolve, reject); } catch (err) { reject(err); } }, 0); } // 异步情况 if (this.state === 'pending') { this.onFullFilledCallbacks.push(() => { setTimeout(() => { try { x = onFullFilled(this.value); resolvePromise(p2, x, resolve, reject); } catch (err) { reject(err); } }, 0); }); this.onRejectedCallbacks.push(() => { setTimeout(() => { try { x = onRejected(this.reason); resolvePromise(p2, x, resolve, reject); } catch (err) { reject(err); } }, 0); }); } }) return p2; // 返回全新的promise } } function resolvePromise(p2, x, resolve, reject) { let called; if (p2 === x) { return reject(new TypeError('typeErr')); } if ((typeof x === 'object' && x !== null) || typeof x === 'function') { try { let then = x.then; if (typeof then === 'function') { then.call(x, y => { if (called) return; called = true; // console.log(y); // resolve(y); resolvePromise(p2, y, resolve, reject); }, r => { if (called) return; called = true; // console.log(r) reject(r); }) } else { if (called) return; called = true; resolve(x); } } catch (err) { if (called) return; called = true; reject(err); } } else { resolve(x); } } const p1 = new MyPromise((resolve, reject) => { // setTimeout(() => { resolve(1); // reject(1); // }, 1000) }); const p2 = p1.then((res) => { // console.log(res); // 这里希望能够链式操作、返回一个promise return new MyPromise((resolve, reject) => { resolve(new MyPromise((resolve, reject) => { resolve(new MyPromise((resolve, reject) => { resolve(new MyPromise((resolve, reject) => { resolve(1000); })); })); })); }) }, (err) => { console.log(err); // return err + 2; }); p2.then((res) => { // 希望res能够拿到上一次回调的结果 console.log(res, 'success'); }, (err) => { console.log(err); }); MyPromise.defer = MyPromise.deferred = function() { let dfd = {}; dfd.promise = new MyPromise((resolve, reject) => { dfd.resolve = resolve; dfd.reject = reject; }); return dfd; }; // 安装promises-aplus-tests工具测试 module.exports = MyPromise;
-
问题检测
安装promises-aplus-tests:
npm i promises-aplus-tests -g
终端输入如下命令进行检测:
promises-aplus-tests index.js
输出这样就说明代码没有问题
23. JavaScript模块化
23.1 历史问题
- 在最开始所有脚本都写在script标签中
- 随后将脚本写在JS文件然后引入,由于共用了一个作用域就会产生变量覆盖=>变量重名=>污染全局的问题
- 于是就产生了立即执行函数,但是依旧无法解决JS加载顺序的问题
模块化解决的问题:1. 加载顺序;2. 污染全局
23.2 立即执行函数的来历
- 函数声明不是表达式,后面不能跟执行符号,只要是表达式都可以
- 只要作用域一加载就函数就立马执行
- 立即执行函数如果前面不打分号会报错,建议首尾都打上
- 有自己的作用域和执行期上下文,可以用来创建一个模块的独立作用域
- 可以通过对象抛出数据,这些数据源自于闭包,而不是全局
- 立即执行函数可通过注入形参的方式来实现接收外部变量,从此解决了污染全局以及模块之间的相互依赖的问题,但仍然无法解决加载顺序的问题。
并不是只要在全局声明变量就是全局污染,声明数据才是,用立即执行函数来声明模块不是
23.3 插件化开发(也是模块化开发的一部分)
也是利用立即执行函数实现,只不过给用户提供了一个配置项,可以根据需要进行配置,还有采用的是面向对象的编程方式
23.4 CommonJS
-
概念
一种成熟的模块化方式,不再通过JS含有的功能区实现模块化,真正解决了加载顺序的问题
不再依赖JS文件,通过CommonJS规范的导入和导出实现相互之间的依赖,需要运行在Node环境里面
-
语法
require('...'); // 引入模块 module.exports; // 导出模块
-
特点
通过CommonJS的导入导出实现模块相互之间的依赖
每引用一个JS文件,就会创建一个模块实例
所有文件加载都是同步进行的
缓存机制:require只能执行一次,只要导入一次就会缓存,如果改了则会比较异同进行更新
是在Node上运行的(写node程序经常使用,客户端开发相对较少)
require引入进来后会变成一个立即执行函数
23.5 AMD
-
概念
Asynchronous Module Definition 异步模块定义
基于CommonJS写的客户端模块化规范
但浏览器仍然不支持,通过require.js实现的AMD才被浏览器支持
-
语法
define(moduleName, [module], factory); //定义模块(模块名,依赖模块,工厂函数) require([module], callback); // 引入模块
-
特点
异步加载模块,所有模块加载完毕才会执行(前置依赖),避免了模块加载顺序的问题
-
示例
index.html
<script src="js/require.js"></script> <script src="js/index.js"></script>
moduleA.js
define('moduleA',function(){ var a = [1,2,3,4,5] return{ a: a.reverse() } })
moduleB.js
define('moduleB',['moduleA'],function(moduleA){ var b = [6,7,8,9,10] return{ b:moduleA.a.concat(b) } })
moduleC.js
define('moduleC',['moduleB'],function(moduleB){ return{ C:moduleB.b.join('-') } })
index.js
require.config({ paths:{ moduleA:'js/moduleA', moduleB:'js/moduleB', moduleC:'js/moduleC' } }) require(['moduleA','moduleB','moduleC'],function(moduleA,moduleB,moduleC){ console.log(moduleA.a); console.log(moduleB.b); console.log(moduleC.c); })
输出:
23.6 CMD
-
概念
Common Module Definition 通用模块定义
由阿里巴巴开发
也无法在浏览器上使用,需要通过sea.js来实现
-
语法
define(function(require,exports,module){}); //定义模块(加载模块,导出模块,操作模块) seajs.use([module路径],function(moduleA,moduleB,moduleC){}); // 使用模块
-
CMD与AMD区别
CMD是按需加载的,依赖就近加载,不像AMD前置加载,需要全部把模块加载完,CMD需要的时候才会加载
-
示例
index.html
<script src="js/sea.js"></script> <script src="js/index.js"></script>
moduleA.js
define(function(require,exports,module){ var a = [1,2,3,4,5] return { a:a.reverse() } })
moduleB.js
define(function(require,exports,module){ var moduleA = require('./moduleA'), b = [6,7,8,9,10] return { b:moduleA.a.concat(b) } })
moduleC.js
define(function(require,exports,module){ var moduleB = require('./moduleB'); return { b:moduleB .b.join('-') } })
index.js文件
seajs.use(['moduleA.js','moduleB.js','moduleC.js'],function(moduleA,moduleB,moduleC){ console.log(moduleA.a); console.log(moduleB.b); console.log(moduleC.c); });
输出:
23.7 ES6Module
-
概念
ECMA官方推出的模块化规范,不同于前几种模块化,它们都是民间、社区开发出来的(CommonJS-node,AMD-民间,CMD-阿里,ES5模块-JS立即执行)
-
语法
import module from '模块路径'; // 导入模块 export.module; // 导出模块
-
示例
index.html
<script src="js/index.js"></script>
moduleA.js
exprot default { a:[1,2,3,4,5].reverse() }
moduleB.js
import moduleA from './moduleA' exprot default { b:moduleA.a.concat([6,7,8,9,10]) }
moduleC.js
import moduleB from './moduleB' exprot default { c:moduleB.b.join('-'); }
index.js
import moduleA from './moduleA'; import moduleB from './moduleB'; import moduleC from './moduleC'; console.log(moduleA.a); console.log(moduleB.b); console.log(moduleC.c);
输出:
23.8 ES6Module与CommonJS区别
-
示例
export.js
export.a = 0; setTimeout(()=>{ console.log('来自export',++exports.a); },300);
common.js
const {a} = require('./exprot') setTimeout(()=>{ console.log('来自common.js',a); },500) // 来自common.js 0(拷贝a的值)
es6.js
import{a} from './exprot'; setTimeout(()=>{ console.log('来自es6',a); },500) // 来自es6 1(拿的a的引用)
-
区别:
-
CommonJS是服务端模块化规范,ES6是客户端模块化规范
-
CommonJS导入的模块成员是该成员的拷贝,而ES6导入的模块成员是该成员的引用
-
CommonJS是运行时加载模块,而ES6是Webpack编译时加载模块
-
24. 生成器与迭代器的应用
24.1 遍历
遍历的核心就是从一个容器里面东西都拿出来
每一次遍历的过程中,要对遍历的结果进行处理,而不是每一次都要处理的情况,那么用遍历本身是不合适的
24.2 迭代与遍历区别
-
遍历
把容器里的所有东西都观察一遍、拿一遍
-
迭代
在本次遍历的过程中进行一次程序上的输出
在遍历的过程当中,循环到当前这一次的时候,就是整个循环功能的一次迭代
项目迭代:根据本次的结果进行一次更新,进行一次修复,进行一次小的功能叠加
在遍历过程当中进行某一次程序的输出的话,那么就必须要有一个叫迭代器的东西
24.3 迭代器(iterator)
迭代器可以在遍历的时候一次一次的执行,而不是整个都执行,它会根据你的指令去遍历指定次数
但它有一个问题:就是迭代器还不能直接使用,因为迭代器是建立在遍历的基础上的,不可能遍历某一次,需要借助生成器
24.4 生成器(generator)
-
概念
就是生产迭代器的东西
由于代器是建立在遍历的基础上的,而生成器就是利用遍历造出一个迭代器
-
生成器及yield
这就是一个生成器,实例化出来的就是一个迭代器
function * test() {}
但它还是需要依赖遍历,它生产迭代器的关键点就在于yield,因为yield可以让这个遍历停下来
var arr = [1, 2, 3, 4, 5, 6]; function * test(arr) { for (var item of arr) { yield item; } } let iterator = test(arr); // 实例化(生产)出来一个迭代器,传入迭代对象
-
next
对迭代器进行迭代,返回值是
{ value: xxx, done: boolean }
(value是抽取的值(对应yield产出的值),done则是否迭代完毕)var arr = [1, 2, 3, 4, 5, 6]; function* test(arr) { for (var item of arr) { yield item; } } let iterator = test(arr); 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: 4, done: false } console.log(iterator.next()); // { value: 5, done: false } console.log(iterator.next()); // { value: 6, done: false } console.log(iterator.next()); // { value: undefined, done: true }
24.5 ES5实现生成器
var arr = [1, 2, 3, 4, 5, 6];
function generator(arr) {
var i = 0;
return {
next() {
var done = i > arr.length ? true : false,
value = done ? 'undefined' : arr[i++];
return {
value: value,
done: done
}
}
}
}
let iterator = generator(arr);
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: 4, done: false }
console.log(iterator.next()); // { value: 5, done: false }
console.log(iterator.next()); // { value: 6, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
24.6 需求
-
从test1依次执行到test5
var functions = [ function test1() { console.log('test1'); }, function test2() { console.log('test2'); }, function test3() { console.log('test3'); }, function test4() { console.log('test4'); }, function test5() { console.log('test5'); } ]; for (let item of functions) { item(); } // test1 // test2 // test3 // test4 // test5
-
执行到某一个地方截断(执行到test3停止 )
var functions = [ function test1() { console.log('test1'); return true; }, function test2() { console.log('test2'); return true; }, function test3() { console.log('test3'); return false; }, function test4() { console.log('test4'); return true; }, function test5() { console.log('test5'); return true; } ]; for (let item of functions) { if (!item()) { break; }; } // test1 // test2 // test3
这种截断是不可取的,因为它是建立在遍历的基础上的,而我们必须建立在迭代的基础上
24.7 中间件(node express)
中间件集合:[test1,test2,test3,test4](token是否存在 => token是否合法 => token是否过期 => 打开页面)
当用户访问一个路由 /user
的时候,这些中间件集合就会执行
但只要其中一个中间件有问题,就会截断后续中间件的执行
在学习express、koa的时候,你会发现每一个中间件函数里面都会存在一个next的东西,也就是说在next执行的时候才会去执行下一个中间件函数
var functions = [
function test1(next) {
console.log('test1');
next(); // 如果中间有哪一个next没有执行,那么后续的中间件是不会执行的,这就是迭代过程中的截断
},
function test2(next) {
console.log('test2');
next();
},
function test3(next) {
console.log('test3');
next();
},
function test4(next) {
console.log('test4');
next();
},
function test5(next) {
console.log('test5');
next();
}
];
24.8 生成器实现中间件
-
源码实现
;(function(functions) { function* generator(arr) { for (var i = 0; i < arr.length; i++) { yield arr[i]; } } var iterator = generator(functions); var init = () => { nextDo(iterator.next()); } function nextDo(n) { n.value(function() { // 这个函数其实就是next,为什么要传这个函数呢,因为它是连续执行的关键,如果next有执行就继续往下执行,如果没有就截断,而继续往下执行就是通过nextDo递归的方式实现的,递归时会判断是否迭代完成,如果否则继续迭代,如果是就终止递归 var n = iterator.next(); // 拿到当前的迭代器对象 if (!n.done) { // 判断时候迭代完成,如果没有就继续迭代,如果是就终止递归 nextDo(n); } else { return } }); } init(); })( [ function test1(next) { console.log('test1'); next(); }, function test2(next) { console.log('test2'); next(); }, function test3(next) { console.log('test3'); // 判断是否有token,有就往下执行,没有就什么都不做 //if(token){ // next(); //} next(); }, function test4(next) { console.log('test4'); next(); }, function test5(next) { console.log('test5'); next(); } ] ); // test1 // test2 // test3 // test4 // test5
-
插件封装及使用
// 导入中间件插件 import M from "./middleware.js"; M([checkInputValue, submitData, loginSuccess]) // 表单验证 function checkInputValue(){ if(......) { next(); } } // 登录提交 function submitData() { $.ajax({ ...... success: function(data){ if(xxxx) { next(); } } }) } // 登录成功 function loginSuccess() { location.href = '......' }
24.9 操作日志
index.html
<input type="text" id="content">
<button id="btn">操作</button>
<ul class="log-list"></ul>
<script src="./index.js"></script>
index.js
;(() => {
var oContent = document.getElementById('content'),
oBtn = document.getElementById('btn'),
oList = document.getElementsByClassName('log-list')[0];
let log = [],
it = generator(log);
const init = () => {
bindEvent();
}
function bindEvent() {
oBtn.addEventListener('click', handleBtnClick, false);
}
function handleBtnClick() {
const value = oContent.value;
log.push({
value,
dateTime: new Date()
})
_addLog(it.next().value);
}
function _addLog(log) {
const oLi = document.createElement('li');
oLi.innerHTML = `
<p>增加一项:${log.value}</p>
<p>操作事件:${log.dateTime}</p>
`
oList.appendChild(oLi);
}
function* generator(arr) {
for (let item of arr) {
yield item;
}
}
init();
})();
迭代器的迭代是有记忆的,它会基于上一次进行下一次迭代,就不用考虑用下标,这也是它好用的地方
生成器、迭代器用得更多的还是底层封装,比如TJ的co库就用了很多(异步转同步的库),以后学了node可以多看看