目录
- ECMAScript、JavaScript、NodeJs,他们的区别是什么?
- ECMAScript有哪些关键的版本
- ES为何如此重要?
- 声明变量的问题
- ES6中的变量声明
- 字符串码元和码点的概念
- ES6中新增的字符串API
- 参数默认值
- 剩余参数
- 展开运算符
- 函数柯里化
- 明确函数的双重用途
- 箭头函数
- 定时器使用注意点
- 新增的对象字面量语法
- 构造函数Object新增的API
- 什么是实例
- 面向对象简介
- 传统的构造函数的问题
- **类class**
- **类的继承**
- 冷知识
- **对象解构**
- 通过解构实现变量值互换
- 函数参数解构
- 普通符号
- 共享符号
- 知名符号
- 事件循环
- 事件和回调函数的缺陷
- **Promise**
- promise其他的API
- 手写Promise
- async 和 await
- async
- await
- async和await的细节注意
- promise改造定时器
- **FetchAPI的概述**
- Fetch基本使用
- **迭代器**
- 迭代器协议
- for-of 循环
- 展开运算符与可迭代对象
- 迭代器和可迭代对象的关系
- 生成器
- 生成器的小练习
- 更多的集合类型
- map集合
- WeakSet和WeakMap
- 回顾属性描述符(非ES6内容)
- Reflect 反射
- Proxy 代理
- 观察者模式
- 新增的数组API
- 静态方法
- 实例方法
- 类型化数组
- ArrayBuffer
- 读写ArrayBuffer
- 实例方法
- 类型化数组
- ArrayBuffer
- 读写ArrayBuffer
ECMAScript、JavaScript、NodeJs,他们的区别是什么?
ECMAScript:简称ES,是一个语言标准(循环、判断、变量、数组等数据类型)
JavaScript:运行在浏览器端的语言,该语言使用ES标准。ES + webAPI = JavaScript webAPI也就是指dom元素和dom对象或事件方法
NodeJs:运行在服务器端的语言,该语言使用ES标准。ES + nodeAPI = NodeJS
简单理解:无论JavaScript 还是NodeJS,他们都是ES的超集(也就是指 这两个语言都包含有ES,并且还有一些自己的API)
ECMAScript有哪些关键的版本
ES3.0:1999
ES5.0:2009
ES6.0:2015,从该版本开始,不再使用数字作为编号,而是用年份 也就是ES2015
ES7.0:2016
ES为何如此重要?
ES解决了JS无法开发大型应用的语言层面的问题。
声明变量的问题
使用var声明的变量
允许重复的声明变量:导致数据被覆盖
变量提升:怪异的数据访问、闭包问题
全局变量挂载到全局对象:全局对象成员污染
ES6的解决办法
不再使用var声明变量
使用 let 声明变量
ES6中的变量声明
let---------------------------------
声明的变量,不允许在 当前作用域范围 内重复声明(重复声明了的话,就会报错)
在块级作用域中,用let定义的变量,在作用域外是不能访问的(也就是作用域里面可以访问外面的变量,但是外部的不能访问里面的变量)
块级作用域:代码执行时遇到花括号,会创建一个块级作用域,花括号结束时,块级作用域会被销毁
可以简单认为 不会变量提升(声明变量前 被调用,就会报错)
全局变量不会挂载到全局对象上,解决全局对象成员污染的问题
在循环中(如for循环),用let定义的循环变量,会特殊处理,每次进入循环体,都会开启一个新的作用域(保存当前循环变量的值),并且将循环变量绑定到该作用域中,并且在循环中用let声明的循环变量,在循环结束后会被销毁。
(也就是每次循环,使用的是一个全新的循环变量)
关于let的拓展
底层实现上,let声明的变量实际上也会提升,但是,提升后会将其放进 "暂时性死区" ,如果访问的变量位于"暂时性死区",则会报错:
"Cannot access 'a' before initialization"
当代码运行到该变量的声明语句时,会将其在"暂时性死区"中移除
块级作用域的演示
let a = 10; //在全局作用域定义的a
{
let a = 20; //在块级作用域定义的a
console.log(a);//20
}//块级作用域执行结束后 就会被销毁
console.log(a);//10
const----------------------------
const和let完全相同,仅在于用const声明的变量,在声明时必须赋值,而且不可以重新赋值。
在实际开发中,应该尽量使用const来声明变量,以保证变量的值不被随意篡改(如dom对象的变量,一般来说是不会更改的)原因如下:
1.根据经验,开发中的很多变量,都是不会更改的,也不应该更改。
2.后续的很多框架或者是第三方JS库,都要求数据不可变,使用常量可以一定程度上保证这一点。
需要注意的细节
1.常量不可变,是指声明的变量内存空间不可变,并不保证内存空间中的地址指向的其他空间不可变。(如对象里面的属性值)
2.常量的命名:
a.特殊的常量:该常量从字面意义上,一定是不可变的,如圆周率、月地距离或其他一些绝不可能变化的配置。
通常,该常量的名称全部使用大写,多个单词之间用 _ 分割
b.普通的常量:使用和之前一样的命名即可
3.在for循环中,循环变量不可使用 常量!!
注意
let和const声明的变量,不能跟形参重名,否则会报错(其实因为形参也跟let或则const声明的一样,后面默认值拓展有提到)
开发中的思路
养成良好习惯:
在文档需要声明变量的时候 最好先用 const声明
到后来,发现这个变量是需要改变的,再将这个变量改成用 let声明
字符串码元和码点的概念
早期,由于存储空间宝贵,Unicode使用16位二进制来存储文字。我们将一个16位的二进制编码叫做一个码元(Code Unit)。
后来,由于技术不断发展,Unicode对文字编码进行了扩展,将某些文字扩展到了32位(占用两个码元),并且,将某个文字对应的二进制数字叫做码点(Code Point)。码点 有可能是16位 也有可能是32位
字符串中length的计算问题
实际上是按照Unicode编码所占用的码元进行计算的,下面的文字 占用了2个码元(32位)
const str = '𠮷';
console.log(str.length);//2
console.log(/^.$/.test(str));//false 因为正则表达式也是按照码元来匹配的,而这里的正则只能匹配到16位的文字
//也就是说,如果要匹配到这个文字,需要按照匹配两个文字的格式来编写正则
字符串中想获取第一个码元,有一个方法: str.charCodeAt(0);str.charCodeAt(1);//返回第一个码元,和第二个码元,而且值不同
//也就证明该文字由两个码元组成
所以导致无法准确的计算出字符串的长度
ES6中解决了length计算问题
ES6中,给出了一个获取字符串码点的方法 codePointAt( )
根据字符串码元的位置得到码点
const str = '𠮷';
console.log(str.codePointAt(0));//方法会根据文字的第一个码元的数值判断是16位还是32位,然后返回相对应的码点
封装一个方法判断字符是否是32位的
function is32bit(char) {
return char.codePointAt(0) > "0xffff";//如果码点大于16进制的最大值
}
ES6中解决正则匹配字符串问题
ES6位正则表达式添加了一个flag :u ,如果添加了该设置,则匹配时,使用码点匹配,如: /^.$/u
ES6中新增的字符串API
includes
判断字符串中是否包含指定的子字符串
语法: str.includes('a',0);//表示在第0位开始往后查找是否含有 'a' 不填索引 默认全局查找
const text = '成哥是个狠人';
const result = text.includes('狠');//返回 true / false
console.log(result);//true
startWith()
str.startWith('a',0);//从第0位字符开始,判断字符串是否以 'a'开头
endWith()
str.endWith('a');//判断字符串是否以 'a' 结尾
repeat()
str.repeat(4);//将str字符串重复4次,并返回结果
正则中的粘连标记
标记名: y
用法:/^.$/y
含义:匹配时,完全按照正则对象的lastIndex位置开始匹配,并且匹配的位置必须在lastIndex位置
模板字符串
ES6之前处理字符串繁琐的两个方面
1.多行字符串
2.字符串拼接
ES6提供了模板字符串的书写,可以方便的换行和拼接,要做的,仅仅是将字符串的开始或结尾改为
反引号
通过${}进行传参和计算
里面填的值是表达式,所以可以是计算公式
表达式是可以嵌套的 ${'123'${'456'}}
如果需要显示${},可以使用转义字符 \${},这样就能在文档上显示'${}'
参数默认值
在书写形参时,直接给形参赋值,赋的值即为默认值
当调用函数时,如果没有给对应的参数赋值 (给它的值是undefined),则会自动使用默认值
注意:null不行,只有传undefined才会使用默认值
function foo(a,b = 1,c = 2){//设置形参b的默认值为1,形参c的默认值为2
return a+b+c;
}
console.log(foo(1));//4
拓展
对arguments的影响
只要给函数加上参数默认值,该函数就会自动变成严格模式下的规则:
形参与实参(arguments)相分离
留意暂时性死区
形参和ES6中的let或const声明一样,具有作用域,并且根据参数的声明顺序,存在暂时性死区(也就是不能在参数未定义之前使用)
也就意味着,let或则const声明的变量,不能跟形参重名,否则报错
剩余参数
arguments的缺陷
1.如果和形参配合使用,容易导致会混乱
2.从语义上,使用arguments获取参数,由于形参缺失,无法从函数定义上理解函数的真是意义(也就是不知道要不要传参,传什么参数)
剩余参数
剩余参数解决了上述问题
作用:
专门用于收集末尾的所有参数,将其放到一个形参数组中
语法
在括号的最后参数位,添加一个形参作为收集参数的数组,形参前面加上 … 就表示剩余参数
字面意思理解:就是省略号,表示不知道会有多少个参数
function joo(...形参名){};
实例:
function foo(...a){
//...a收集了所有参数,形成的一个数组
console.log(a);//[1,2,3,4,5]
console.log(a[0]);//1
}
foo(1,2,3,4,5);
注意
1.一个函数,仅能出现一个剩余参数
2.一个函数,如果有剩余参数,剩余参数必须是最后一个参数
有了剩余参数后,后面就基本可以不使用arguments了
展开运算符
需求
有时候,有些函数可能需要传递多个参数
但是,你获取到的数据是一个数组
那么这时候就需要将每个数组拆开成多个的参数传递给函数
此时就需要使用 展开运算符了
展开运算符
语法
格式与剩余参数类似,也是以 …实参名 进行传值
但是,这是用于传递数据的,而剩余参数是用于接收数据的
const arr = [1,2,3,4];
function sum(...list){// ...list是剩余参数
let sum = null;
for(let i = 0; i < list.length; i++){
sum += list[i];
}
return sum;
}
sum(...arr);// ...arr是将arr数组展开成多个元素进行传值,相当于: sum(1,2,3,4)
展开运算符没有规定只能在最后传值,哪个实参位置都能使用
作用
可以用于克隆数组
const arr1 = [1,2,3,4];
const arr2 = [...arr1];//arr2克隆了arr1
ES7的展开运算符能在对象上使用
const obj1 = {
name:'小白',
age:18,
list:[1,2,3,4,5],
son:{
name:'小黄',
age:1
}
}
const obj2 = {
...obj1,//这样就实现了克隆对象obj1了
list:[...obj1.list],
son:{
...obj1.son//遇到应用类型的属性,需要再次展开赋值才可以实现深拷贝,不然只会赋值地址指向
}
}
注意:展开运算符的克隆,只能克隆对象的属性地址指向,也就是说是浅克隆
也就是指两者的属性所指向的地址是相同的,如果克隆后改变地址里面的数据(应用类型),则两者都会跟着改变
函数柯里化
柯里化:用户固定某个函数的前面的参数,得到一个新的函数(newCal),新的函数调用时(newCal(3,4)),接收剩余的参数
function cal(a, b, c, d) {
return a + b * c - d;
}
//柯里化:用户固定某个函数的前面的参数,得到一个新的函数(newCal),新的函数调用时(newCal(3,4)),接收剩余的参数
function curry(func, ...args) { //...args --->1,2
return function(...subArgs) { //...subArgs --->3,4
const allArgs = [...args, ...subArgs];
if (allArgs.length === func.length) {
return func(...allArgs);
} else {
return curry(func, ...allArgs);//再返回一次函数接收参数,以补足参数调用函数
}
}
}
const newCal = curry(cal, 1, 2); //a,b 固定一个值进行传递
console.log(newCal(3, 4)); //1+2*3-4
console.log(newCal(5)(6)); //1+2*5-6
明确函数的双重用途
也就是判断,该函数是当作构造函数使用还是普通函数调用
ES6一个特殊的API —new.target
可以使用这个API在函数内部,判断该函数是否使用了new来调用
该表达式,得到的是:
如果没有使用new来调用函数,则返回undefined
如果使用new调用函数,则得到的是new关键字后面的函数本身
function Father() {
if (!new.target) {//如果返回的值是undefined
throw new Error("没有使用new调用方法")
}
this.name = '小白';
this.age = 18;
}
const son1 = new Father();
const son2 = Father();
箭头函数
回顾this指向
1.通过对象调用函数:this指向对象
2.直接调用函数:this指向全局对象
3.通过new调用函数:this指向新创建的对象
4.通过call、apply、bind调用函数:this指向指定的数据
5.如果是DOM事件函数:this指向事件源(也就是给谁注册的事件,this就指向谁)
箭头函数
箭头函数是一个表达式
意味着他不会被预编译过程提升上去
理论上,任何使用函数表达式的场景都可以使用箭头函数
使用语法
去掉function
在() 后加上箭头 =>
const func = (参数1,参数2,...) => {
//函数体
}
注意的细节
1.箭头函数中,不存在 this、arguments、new.target,如果使用了,则使用的是:定义函数外层作用域对应的 this、arguments、new.target
2.如果参数只有一个,可以省略小括号
const func = 参数 => {}
3.如果箭头函数只有一条返回语句,可以省略大括号,和return关键字
const func = 参数 => 返回值
4.如果箭头函数只有一条返回语句,并且返回的是对象,则可以在对象的大括号外再套一层小括号,表示表达式,这样程序就能理解了
const func = 参数 => ({
name:'小白',
age:18
})
5.箭头函数没有原型,undefined
6.由于没有原型,所以箭头函数不能作为构造函数使用
应用场景
1.临时性使用的函数,并不会直接调用它,比如:
1.事件处理函数(dom事件)
2.异步处理函数(setInterval、setTimeout)
3.其他临时性的函数
2.为了绑定外层this的函数
3.在不影响其他代码的情况下,保持代码的简洁,最常见的:数组方法中的回调函数(fillter、map、reduce等)
注意:对象的属性是不需要用箭头函数的,他会导致箭头函数的this指向对象外层的this,并不是指向对象
注意
定时器使用注意点
当定时器函数是function时
const obj = {
say(){
this.timer = setTimeout(function(){
console.log(this);
},100)
}
}
obj.say();
// 输出的this 是 全局对象 window
// 因为定时器里面的函数 是由js引擎调用的(独立调用),所以会指向window
当定时器函数是箭头函数时
const obj = {
say() {
this.timer = setTimeout(() => {
console.log(this);
}, 100)
}
}
obj.say();
//输出的this 是say函数里面的this,指向obj
//因为箭头函数是没有this的,他只会保留定义箭头函数时,外层的this(也就是say的this)
新增的对象字面量语法
对象字面量就是指,初始化对象时,如
const obj = {
name:'小白',
age:18
}
这种就是字面量初始化对象
成员速写
原本的语法
function createPerson(username,age,sex){
return {
username:username,
age:age,
sex:sex
}
}
成员速写语法
如果对象字面量初始化时,成员的名称来自于一个变量,而这个变量的名字和成员名称相同,则可以进行简写,如下:
function createPerson(username,age,sex){
return {
username,
age,
sex
}
}
方法速写
对象字面量初始化时,方法可以省略冒号和function关键字
//原本写法
const obj = {
name:'小白',
say:function(){
console.log(this.name);
}
}
//速写
const obj = {
name:'小白',
say(){
console.log(this.name);
}
}
计算属性名
有的时候,初始化对象时,某些属性名可能来自于某个表达式的值,在ES6中,可以使用中括号来表示该属性名是通过计算得到的
const prop1 = 'name';
const prop2 = 'age';
const prop3 = 'sex';
const prop4 = 'say';//say方法
const obj = {
[prop1]:'小白',
[prop2]:18,
[prop3]:'男',
[prop4](){
console.log(this[prop1],this[prop2]);
}
}
中括号里面返回的是该表达式的值
构造函数Object新增的API
Object.is
用于判断两个数据是否相等,基本上跟严格相等(===)是一致的,除了一下两点:
//===判断
NaN === NaN ;//false
+0 === -0 ;//true
//Object.is判断
1. NaN 和 NaN相等
Object.is(NaN,NaN);//返回true
2. +0 和 -0相等
Object.is(+0,-0);//返回false
Object.assign
对象覆盖
可以传任意个参数,方法使用的是剩余参数
方法只会对第一个参数产生改动,并返回第一个参数
const obj1 = {
name:'小白',
age:18
}
const obj2 = {
name:'小黑',
age:19
}
//将obj2的数据,覆盖到obj1,并且会对obj1产生改动,然后返回obj1
const obj = Object,assign(obj1,obj2);
//可以把第一个参数换成是{},这样最后改动的只是{},其他对象不会被改动
const obj = Object.assign({},obj1,obj2);
建议不用这个,使用展开运算符比较好,不然改动对象有可能会影响后面使用的框架
Object.getOwnPropertyNames
枚举对象属性名,返回一个数组
枚举顺序
Object.getOwnPropertyNames方法之前就存在,只不过,官方没有明确要求,堆属性的顺序如何排序,如何排序,完全由浏览器厂商决定
ES6规定了该方法返回的数组的排序方式如下:
先排数字,并按照升序排序
再排其他,按照书写顺序排序
Object.setPrototypeOf(obj1,obj2)
设置obj1的原型指向obj2
设置的是 proto
比较少用到
const obj1 = {
a:1
}
const obj2 = {
b:2
}
Object.setPrototypeOf(obj1,obj2);
console.log(obj1.b);
什么是实例
实例就是由构造函数创建出来的对象。
function Person() {}// 此函数为构造函数
var p=new Person(); // p为构造函数创建出来的对象
实例成员和静态成员
实例成员:由构造函数创建出来的对象能直接访问的属性和方法,包括:对象本身 以及原型中的所有的属性和方法。
例如:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function() {
console.log("hello, rose");
};
var p = new Person("jack", 19);
// ( p.name p.age p.sayHi) 括号内三个就是实例成员了。
静态成员:由构造函数直接访问到的属性和方法。大家注意是直接访问的属性和方法,间接获取就不是了。
function Person(name, age) {
this.name = name;
this.age = age;
}
var p = new Person("jack", 19);
Person.say = function() {alert("这是静态方法");};
// Person.say 就是静态成员
//Person.length 也是静态成员,因为length是函数中的 方法,是来直接获取函数中形参的个数。
面向对象简介
面向对象:一种编程思想,更具体的语言
对比面向过程:
以把大象转进冰箱为例
面向过程:思考的切入点是功能的步骤-------------------------------------
第一步:把冰箱门打开
第二步:把大象装进去
第三步:关闭冰箱门
面向对象:思考的切入点是对象的划分-------------------------------------
有两个对象:大象、冰箱
分别对这两个对象进行功能描述,如:大象会走路、会吃东西,冰箱会制冷、能自动开门关门等功能
function Elethons(){};//大象
Elethons.prototype.move = function(){};//赋予功能
function Frige(){};//冰箱
Frige.prototype.cook = function(){};//赋予功能
传统的构造函数的问题
1.属性和原型方法定义分离,降低了可读性
2.原型成员可以被枚举
3.默认情况下,构造函数仍然可以被当作普通函数使用
类class
//面向对象中,将 下面对一个对象的所有成员的定义,统称为类
//构造函数 构造器
function Animal(type, name, age, sex) {
this.type = type;
this.name = name;
this.age = age;
this.sex = sex;
}
//定义实例方法(原型方法)
Animal.prototype.print = function() {
console.log(`种类:${this.type}`);
console.log(`名字:${this.name}`);
console.log(`年龄:${this.age}`);
console.log(`性别:${this.sex}`);
}
const dog = new Animal('狗', '二哈', '2', '公');
dog.print();
类的特点
1.类声明不会被提升,与let、const一样,存在暂时性死区
2.类中的所有代码均在严格模式下执行
3.类的所有方法都是不可枚举的
4.类的所有方法都无法被当作构造函数使用
5.类的构造器必须使用 new 来调用
语法
class newAnimal {
constructor(type, name, age, sex) {//相当于构造器 构造函数 这里面定义的属性最后会挂载到实例对象上
this.type = type;
this.name = name;
this.age = age;
this.sex = sex;
}
print() {//class里面定义的方法最后都会挂载到原型上,并且函数写法类似速写
console.log(`种类:${this.type}`);
console.log(`名字:${this.name}`);
console.log(`年龄:${this.age}`);
console.log(`性别:${this.sex}`);
}
}
const dog2 = new newAnimal('狗', '二哈', '2', '公');
dog2.print();
如果constructor没有参数,是空的,可以不写
类的其他书写方式
1.可计算的成员名
就如成员速写一样
const prop1 = 'say';
class Son{
constructor(name){
this.name = name;
}
get name(){
return this._name;
}
set name(name){
name = '吴'+name;
this._name = name;
}
//直接用表达式代替方法名
[prop1](){
console.log(this.name)
}
}
const son = new Son('讲得');
son[prop1]();//吴讲得
2.getter和setter
在回顾属性描述符中有详解
Object.defineProperty 可定义某个对象成员属性的读取和设置
使用getter 和 setter控制的属性,不在原型上(是普通对象的方法)
ES6的写法:
get 属性名(){}
set 属性名(value){}
例子
class newAnimal {
constructor(type, name, age, sex) {
this.age = age;
this.name = name;
this.type = type;
//原本ES5的属性访问器写法
// Object.property(this,'type',{
// set(value){},
// get(){}
// })
this.sex = sex;
}
//访问器: get() 和 set()
get type() { //一旦需要获取属性,就会调用get()
return this._type;
}
set type(type) { //一旦需要设置属性时,就会调用set()
if (type === '狗') {
type = '单身狗'
} else {
type = this.type;
}
this._type = type;
}
print() {
console.log(`种类:${this.type}`);
console.log(`名字:${this.name}`);
console.log(`年龄:${this.age}`);
console.log(`性别:${this.sex}`);
}
}
const dog2 = new newAnimal('狗', '二哈', '2', '公');
dog2.print();
3.class的静态成员和实例成员
静态成员:只能通过构造函数本身来访问(就像obj.age,只有obj才能访问到age属性)
//静态成员 语法: 用关键字 static定义
//静态成员一般是固定不变的值,便于后续使用
class Father {
constructor(name){
this.name = name;//实例成员
}
//声明静态成员
static sex = '男';
static age = '18';
//静态方法
static say(){
console.log('性别:'+this.sex);//用Father代替this也可以
}
//实例方法
print(){
console.log('姓名:'+this.name);
}
}
const son = new Father('小白');
son.print();//输出 姓名:小白
Father.say();//输出 性别:男
实例成员:由构造函数创建出来的对象能直接访问的属性和方法,包括:对象本身 以及原型中的所有的属性和方法
4.字段初始化器(ES7)
当遇到下面这种情况,可以使用字段初始化器书写
如果一开始就已经赋值了的,可以使用字段初始化器书写 也就是直接在class里面写 : 变量名 = 值
可以理解为:他自动会把变量放到构造函数(constructor)中
class Test{
d = 4;
e = 5
constructor(){
//如果有这种直接就给他赋了值的字段,就可以直接像上面的这样书写(作用是相同的)
this.a = 1;
this.b = 2;
this.c = 3;
//下面字段初始化器的print写法等同于这个写法
//this.print = () => {
//console.log(this.a);
//}
}
//书写方法 这样书写,是为了防止this的指向被改变 导致程序错误
print = () => {//this保存的是函数定义时外层作用域的this,如果是function则是undefined(class的代码均在严格模式下执行)
console.log(this.a);
}
}
const t = new Test();
const p = t.print;
p();//输出1 如果不使用上面的方法,这个调用会报错,因为在严格模式下预编译过程中函数的this是undefined
注意
把方法挂载在实例对象上,会相对消耗内存,但可能并不大
1.使用static的字段初始化器,添加的是静态成员
2.没有使用static的字段初始化器,添加的成员位于实例对象上
3.当使用字段初始化器定义方法后,该方法就不是原型上的了,而是实例对象上的方法了
5.匿名表达式写法
匿名类,类表达式
作用一样
const Son = class{
a = 1;
b = 2;
}
类的继承
判断两个类是否有继承关系
如果两个类A和B,如果可以描述为:B 是 A,则A 和 B形成继承关系。简单举例: 男人 是 人,所以男人和人形成继承关系
如果B 是 A,则有如下描述方式:
1.B继承自A
2.A派生B
3.B是A的子类
4.A是B的父类
如果A是B的父类,则B会自动拥有A中的所有实例成员( 实例方法、实例属性 )
extends关键字实现继承
extends:继承,用于类的定义
super关键字调用父类的构造函数
class Animal {
constructor(type, name, age, sex) {
this.type = type;
this.name = name;
this.age = age;
this.sex = sex;
}
print() {
console.log(this.type + '都会觅食');
}
}
class Dog extends Animal { //使用 extends 继承Animal类
constructor(name, age, sex) {
super('犬科动物', name, age, sex); // 使用 super关键字调用 父类构造函数
}
}
const dog = new Dog('二哈', 2, '雄性');
dog.print();//犬科动物都会觅食
super关键字调用父类构造函数
原理:
通过call(this,参数); 借用父类构造函数,以下代码和上面使用extends关键字的实现继承 相同效果
function Animal(type, name, age, sex) {
this.type = type;
this.name = name;
this.age = age;
this.sex = sex;
}
Animal.prototype.print = function() {
console.log(this.type + '都会觅食');
}
function Dog(name, age, sex) {
Animal.call(this, '犬科动物', name, age, sex); //借用Animal构造函数
//通过 setPrototypeOf 设置Dog.prototype.__proto__指向Animal.prototype 实现继承
Object.setPrototypeOf(Dog.prototype, Animal.prototype);
}
const dog = new Dog('二哈', 2, '雄性');
dog.print();
**作用一:**直接当作函数调用,当作父类构造函数
class Dog extends Animal { //使用 extends 继承Animal类
constructor(name, age, sex) {
//super必须写在第一行
super('犬科动物', name, age, sex); // 使用 super关键字调用 父类构造函数
}
}
**作用二:**如果当作对象使用,则表示父类的原型(prototype)
class Animal {
constructor(type, name, age, sex) {
this.type = type;
this.name = name;
this.age = age;
this.sex = sex;
}
print() {
console.log(this.type + '都会觅食');
}
}
class Dog extends Animal { //使用 extends 继承Animal类
constructor(name, age, sex) {
//super()必须写在第一行
super('犬科动物', name, age, sex); // 使用 super关键字调用 父类构造函数
}
//子类重写print方法
print(){
super.print();
console.log(this.type + '除了会觅食,还很忠心');
}
}
const dog = new Dog('二哈', 2, '雄性');
dog.print();// 输出 犬科动物都会觅食 犬科动物除了会觅食,还很忠心
注意
ES6要求:
如果定义了constuctor,并且该类是子类,则必须在constructor的第一行手动调用父类的构造函数
否则,会导致程序报错
如果没有定义constructor,并且该类是子类,则会有默认的构造函数,该构造器需要的参数和父类一致,并且自动调用父类构造函数
相当于在子类自动添加了以下代码:
constructor(name, age, sex) {
super('犬科动物', name, age, sex);
}
冷知识
用js模拟抽象类
抽象类:一般是父类,不能通过该类创建对象
简单理解: 世界上没有人, 因为根本找不到一个叫人的实物,只有叫什么名字的人,什么性别的人,但是没有人这个东西。也就相当于人其实是指代一个范围,他是一个统称,而实际并不存在
下面,Animal也是一个抽象类,所以不能直接创建Animal的实例对象(new Animal)
class Animal {
constructor(type, name, age, sex) {
if(new.target === Animal){
//判断是否是new调用方法,是的话看看调用者new后面的方法是不是Animal,是的话,报错
throw new TypeError('不能直接调用Animal进行实例对象');
}
this.type = type;
this.name = name;
this.age = age;
this.sex = sex;
}
print() {
console.log(this.type + '都会觅食');
}
}
class Dog extends Animal { //使用 extends 继承Animal类
constructor(name, age, sex) {
//super()必须写在第一行
super('犬科动物', name, age, sex); // 使用 super关键字调用 父类构造函数
}
//子类重写print方法
print(){
super.print();
console.log(this.type + '除了会觅食,还很忠心');
}
}
const dog = new Dog('二哈', 2, '雄性');
dog.print();// 输出 犬科动物都会觅食 犬科动物除了会觅食,还很忠心
this指向
正常情况下,this的指向,this始终指向具体的类的实例对象
对象解构
解构语法和原理
不用解构使用对象里面的属性
const obj = {
name:'小白';
age:18,
sex:'男'
}
let name,age,sex;
name = obj.name;
age = obj.age;
sex = obj.sex;
初步使用对象解构使用对象的属性
const obj = {
name:'小白';
age:18,
sex:'男'
}
//{name,age,sex} = obj; //这样书写 跟上面代码一样效果 但是还不完善
//这样写程序会报错 会认为{}是一条语句,obj的 = 左边缺失东西
//所以需要加上一个小括号括起来 让程序知道这是一个整体
let name,age,sex;
({里面放的是跟属性名同名的变量} = 对象名)
({name,age,sex} = obj);
console.log(name,age,sex);//输出 小白 18 男
最终的简写语法----语法糖
**原理:**先定义需要用的变量,然后从对象中读取同名属性,放到变量中
let {name,age,sex} = obj;
注意:如果定义的变量在对象中找不到对应的属性,那么该变量的值是 undefined
解构的时候不会对被解构的目标造成影响
给解构变量设置默认值
当对象里面没有找到与变量同名的属性时,使用默认值替代
如果能找到,那就会用同名属性的值
语法
let {变量名 = 默认值} = 对象
//比如
let {name,age,sex,abc = 1} = obj;//obj里面没有abc
console.log(abc);//1
非同名写法
当你不想使用跟属性名相同的变量时,可以使用非同名写法
let {属性名:变量名} = obj;
//比如
let {name:username,age,sex,abc = 1} = obj;
//先定义四个变量:username、age、sex、abc
//再从对象obj中读取同名属性赋值(其中,username读取的是name属性)
console.log(username);//'小白'
对于复杂对象的解构
const obj = {
name : '小白',
age : 18,
son : {
sonName : '微白'
}
}
//先定义三个变量 name、age、son
//再解构son对象的sonName
let {name,age,son:{sonName}} = obj;
console.log(name,age,son,sonName); // 小白 18 son对象 微白
数组解构
其实原理跟解构对象一样
可以理解为,数组的属性名是以数字命名的
所以解构 需要用到非同名写法进行解构
const arr = ['a','b','c','d'];
const {
0:n1,
1:n2,
2:n3,
3:n4
} = arr;
console.log(n1,n2,n3,n4);// abcd
语法糖写法
const arr = ['a','b','c','d'];
//它会按照顺序一次赋值给变量
const [n1,n2,n3,n4] = arr;
console.log(n1,n2,n3,n4);//abcd
//如果指向要第三项元素的值
//只需要用 逗号 空开不需要的元素就行了
const [,,n3] = arr;
console.log(n3);//c
数组解构实际上和对象解构一样,所以其他用法都相同,只是获取方式用的是[ ],对象用的是{ }
注意
对象的属性不需要逗号隔开 属性是直接对照名字查找的
练习
const arr = [1,2,3,{a:10,b:20,c:30}];
//解构数组中对象里面的b
const [,,,{b:n}] = arr;
console.log(n);// 20
结合展开运算符解构
注意展开运算符在解构中使用时,只能放到最后面
不然程序就不知道你要展开多少个了
----------------------------对象
const user = {
name:'小白',
age:18,
sex:'男',
address:{
city:'清远'
}
}
//想获取name属性,剩下的属性放到 obj对象中
const {name,...obj} = user;
console.log(name,obj);
//小白 {age: 18, sex: "男", address: {…}}
------------------------------数组
const arr = [1, 2, 3, 4, 5, 6, 7];
//想将前两项 分别放到a,b中,剩余的元素放到数组nums中
const [a, b, ...nums] = arr;
console.log(a, b, nums);
//1 2 (5) [3, 4, 5, 6, 7]
通过解构实现变量值互换
let a = 1,b = 2;
//这样就能实现值互换了
[b,a] = [a,b];
console.log(a,b);// 2 1
函数参数解构
//通过解构的方式进行参数解构,方便函数使用
function foo({
name,
age,
sex,
son: {
sonName
}
}) {
console.log(`姓名:${name}`);
console.log(`年龄:${age}`);
console.log(`性别:${sex}`);
console.log(`儿子姓名:${sonName}`);
}
const user = {
name: '大白',
age: 18,
sex: '男',
son: {
sonName: '小白'
}
}
foo(user);
模拟ajax请求时,设置参数默认值场景
//比如 默认请求方式使用 get
function ajax({
method = 'get',
url
} = {}){//给参数设置默认值 当函数没有传递参数时,就会默认传了 {}
console.log(method,url);
}
ajax({
url:'/abc'
})
//get /abc
注意
undefined、null是不能解构的,会报错
所以如果函数设置了参数解构,那么该函数不传参数就会报错,除非函数参数设置了默认值
普通符号
符号是ES6新增的一个原始值数据类型,它通过使用函数 Symbol(描述信息) 来创建的
描述信息只是方便调试,方便开发者辨别的而已
创建符号
const smb1 = Symbol();
const smb2 = Symbol('abc');
console.log(smb1,smb2);//Symbol() Symbol(abc);
设计的初衷
是为了给对象设置私有属性的
**私有属性:**只能在对象内部使用,外部无法使用
(可以理解为:只想显示有用的功能给别人使用,其他那些辅助性功能(如:获取随机数函数),别人用不到的就设置成私有属性)
符号的特点
1.没有字面量(比如number的字面量:1,2,3等等;对象的字面量:{...};undefined的字面量:undefined
2.使用typeof 返回的值就是 'symnol'
3.每次调用Symbol()函数得到的符号永远不相等,无论符号名是否相同(Symbol()每次返回的值都不相同)
4.符号可以作为对象的属性名存在,这种属性称之为 符号属性 (采用计算属性方式使用)
const syb1 = Symbol('这是一个符号属性');
const obj = {
a : 1,
[syb1]:2
}
console.log(obj);
//{a: 1, Symbol(这是一个符号属性): 2}
5.每个符号都是唯一的(独一无二)
6.符号是不能被隐式转换的,因此不能被用于数学运算、字符串拼接或其他隐式转换的场景。
但是,字符可以显式的转换为字符串,通过String构造函数进行转换,console.log之所以可以输出,就是因为内部使用了String构造函数显式转换
所以从ES6开始,(对象、数组)属性名除了只能用字符串表示外,现在还可以用符号表示
符号属性
用符号作为属性,可以是外部无法使用该属性
const obj = (function() {
//把符号属性 写道return上面
const getRandom = Symbol('这是获取随机数函数');
// const getRandom = 'getRandom';//这样写的话 是可以被外部调用的(因为产生了闭包)
return {
name: '小白',
//我不想让 外面的人可以访问到getRandom(),只想给他们使用luckyNum()
[getRandom]() {
return parseInt(Math.random() * (10 - 1) + 1); //获取1到9的随机数
},
luckyNum() {
return this[getRandom]();
}
}
})()
console.log(obj.luckyNum()); //3
console.log(obj.getRandom()); //报错
console.log(obj[getRandom]()); //报错
符号属性是不能枚举的
因此在 for … in 循环中无法读取到符号属性,Object.keys方法也无法读到符号属性
(Object.keys 是返回一个存储可枚举属性名的数组)
虽然**Object.getOwnPropertyNames()**可以得到所有 可枚举或不可枚举 的属性名,但是 依然得不到符号属性
const syb1 = Symbol();
const obj = {
a: 1,
b: 2,
[syb1]: 3
}
for (var prop in obj) {
console.log(obj[prop]); // 1 2
}
Object.getOwnPropertySymbols(对象)
返回一个存储着对象中符号属性的数组
ES6新增方法,就是为了能够获取符号
所以可以通过这个方法来实现在外部使用符号属性
const obj = (function() {
//把符号属性 写道return上面
const getRandom = Symbol('这是获取随机数函数');
// const getRandom = 'getRandom';//这样写的话 是可以被外部调用的(因为产生了闭包)
return {
name: '小白',
//我不想让 外面的人可以访问到getRandom(),只想给他们使用luckyNum()
[getRandom]() {
return parseInt(Math.random() * (10 - 1) + 1); //获取1到9的随机数
},
luckyNum() {
return this[getRandom]();
}
}
})()
//通过Object.getOwnPropertySymbols(obj) 获取到对象的符号属性数组
const syb1 = Object.getOwnPropertySymbols(obj);
console.log(syb1);
console.log(obj[syb1[0]]());//1
符号是不能被隐式转换的,因此不能被用于数学运算、字符串拼接或其他隐式转换的场景
共享符号
共享符号就是 如果创建的 Symbol(符号描述) 相同
那么就认为是同一个符号
语法
Symbol.for('符号名/符号描述');//获取共享符号
//这里面的for属于Symbol的静态成员
const obj = {
a:1,
[Symbol.for('c')]:2
}
console.log(obj[Symbol.for('c')]);//2
模拟共享符号Symbol.for()
const SymbolFor = (() => {
const global = {};
return function(prop) {
if (!global[prop]) { //如果属性未定义
global[prop] = Symbol(prop);
}
return global[prop];
}
})()
const a = SymbolFor('a');
const b = SymbolFor('a');
console.log(a, b);//Symbol(a) Symbol(a)
知名符号
概念
知名符号是一些具有特殊含义的共享符号,通过Symbol的静态属性得到
ES6中延续了ES5的思想:减少魔法,暴露内部实现
Symbol.hasInstance
参与instanceof()的内部实现
实际上内部是 执行了如下代码
function A(){};
const obj = new A();
obj instanceof A ;//true
//instanceof里面的内部执行代码是下面的语句,效果相同
A[Symbol.hasInstance](obj);//true
这个 Symbol.hasInstance是可以修改的,如果修改了该函数,那么也会影响instanceof改变
修改该函数
Object.defineProperty(A,Symbol.hasInstance,{//Object.defineProperty(修改的对象,属性名,{修改的内容})
value:function(obj){
return false;
}
})
事件循环
执行栈
JS运行的环境称之为宿主环境
执行栈:call stack,一个数据结构,用于存放各种函数的执行环境,每一个函数执行之前,它的相关信息会加入到执行栈。函数调用之前,创建执行环境,然后加入到执行栈;函数调用之后,销毁执行环境。
JS引擎永远执行的是执行栈的最顶部。
浏览器宿主环境中包含5个线程
- JS引擎:负责执行执行栈的最顶部代码
- GUI线程:负责渲染页面
- 事件监听线程:负责监听各种事件
- 计时线程:负责计时
- 网络线程:负责网络通信
异步函数
异步函数:某些函数不会立即执行,需要等到某个时机到达后才会执行,这样的函数称之为异步函数。比如事件处理函数。异步函数的执行时机,会被宿主环境控制。
事件队列
当上面的线程发生了某些事请,如果该线程发现,这件事情有处理程序,它会将该处理程序加入一个叫做事件队列的内存。当JS引擎发现,执行栈中已经没有了任何内容后,会将事件队列中的第一个函数加入到执行栈中执行。
JS引擎对事件队列的取出执行方式,以及与宿主环境的配合,称之为事件循环。
事件队列在不同的宿主环境中有所差异,大部分宿主环境会将事件队列进行细分。在浏览器中,事件队列分为两种:
-
宏任务(队列):macroTask,计时器结束的回调、事件回调、http回调等等绝大部分异步函数进入宏队列
-
微任务(队列):MutationObserver,Promise产生的回调进入微队列
注意
-
当执行栈清空时,JS引擎首先会将微任务中的所有任务依次执行结束,如果没有微任务,则执行宏任务。
事件和回调函数的缺陷
我们习惯于使用传统的回调或事件处理来解决异步问题
事件:某个对象的属性是一个函数,当发生某一件事时,运行该函数
dom.onclick = function(){
}
回调:运行某个函数以实现某个功能的时候,传入一个函数作为参数,当发生某件事的时候,会运行该函数。
dom.addEventListener("click", function(){
})
本质上,事件和回调并没有本质的区别,只是把函数放置的位置不同而已。
一直以来,该模式都运作良好。
直到前端工程越来越复杂…
目前,该模式主要面临以下两个问题:
- 回调地狱:某个异步操作需要等待之前的异步操作完成,无论用回调还是事件,都会陷入不断的嵌套
- 异步之间的联系:某个异步操作要等待多个异步操作的结果,对这种联系的处理,会让代码的复杂度剧增
异步处理的通用模型
ES官方参考了大量的异步场景,总结出了一套异步的通用模型,该模型可以覆盖几乎所有的异步场景,甚至是同步场景。
值得注意的是,为了兼容旧系统,ES6 并不打算抛弃掉过去的做法,只是基于该模型推出一个全新的 API,使用该API,会让异步处理更加的简洁优雅。
理解该 API,最重要的,是理解它的异步模型
- ES6 将某一件可能发生异步操作的事情,分为两个阶段:unsettled 和 settled
- unsettled: 未决阶段,表示事情还在进行前期的处理,并没有发生通向结果的那件事
- settled:已决阶段,事情已经有了一个结果,不管这个结果是好是坏,整件事情无法逆转
事情总是从 未决阶段 逐步发展到 已决阶段的。并且,未决阶段拥有控制何时通向已决阶段的能力。
- ES6将事情划分为三种状态: pending、resolved、rejected
- pending: 挂起,处于未决阶段,则表示这件事情还在挂起(最终的结果还没出来)
- resolved:已处理,已决阶段的一种状态,表示整件事情已经出现结果,并是一个可以按照正常逻辑进行下去的结果
- rejected:已拒绝,已决阶段的一种状态,表示整件事情已经出现结果,并是一个无法按照正常逻辑进行下去的结果,通常用于表示有一个错误
既然未决阶段有权力决定事情的走向,因此,未决阶段可以决定事情最终的状态!
我们将 把事情变为resolved状态的过程叫做:resolve,推向该状态时,可能会传递一些数据
我们将 把事情变为rejected状态的过程叫做:reject,推向该状态时,同样可能会传递一些数据,通常为错误信息
始终记住,无论是阶段,还是状态,是不可逆的!
- 当事情达到已决阶段后,通常需要进行后续处理,不同的已决状态,决定了不同的后续处理。
- resolved状态:这是一个正常的已决状态,后续处理表示为 thenable
- rejected状态:这是一个非正常的已决状态,后续处理表示为 catchable
后续处理可能有多个,因此会形成作业队列,这些后续处理会按照顺序,当状态到达后依次执行
- 整件事称之为Promise
理解上面的概念,对学习Promise至关重要!
Promise
promise的理解
promise并没有消除回调,而是让回调变得可控
promise的基本使用
const pro = new Promise((resolve, reject)=>{
//这里的代码会立即执行 这里主要是为了处理什么时候表示成功,然后调用resolve,什么时候表示失败,调用reject
// 未决阶段的处理
// 通过调用resolve函数将Promise推向已决阶段的fulfilled状态
// 通过调用reject函数将Promise推向已决阶段的rejected状态
// resolve和reject均可以传递最多一个参数,表示推向状态的数据
//resolve(data);
//reject(err);
})
//pro:resolved / rejected
pro.then(data=>{//thenable函数
//这是thenable函数,如果当前的Promise已经是resolved状态,该函数会立即执行
//如果当前是未决阶段,则会加入到作业队列,等待到达resolved状态后执行
//data为状态数据
}, err=>{//catchable函数
//这是catchable函数,如果当前的Promise已经是rejected状态,该函数会立即执行
//如果当前是未决阶段,则会加入到作业队列,等待到达rejected状态后执行
//err为状态数据
})
then()里面放的函数都是异步执行的函数,而且当promise变成resolved或rejected后,会被放到微队列,等待同步代码执行完后再执行微队列的代码
另外一种写法
const pro = new Promise((resolve,reject)=>{
});
pro.then((data)=>{
});
pro.catch((err)=>{
})
细节
- 未决阶段的处理函数是同步的,会立即执行
- thenable和catchable函数是异步的,就算是立即执行,也会加入到事件队列中等待执行,并且,加入的队列是微队列
- pro.then可以只添加thenable函数,pro.catch可以单独添加catchable函数
- 在未决阶段的处理函数中,如果发生未捕获的错误(throw new Error()),会将状态推向rejected,并会被catchable捕获
- 一旦状态推向了已决阶段,无法再对状态做任何更改,所以后面如果还继续调用resolve或reject,也就是无效代码(可以忽略)
- Promise并没有消除回调,只是让回调变得可控
promise串联
当后续的Promise需要用到之前的Promise的处理结果时,需要Promise的串联
Promise对象中,无论是then方法还是catch方法,它们都具有返回值,返回的是一个全新的Promise对象,它的状态满足下面的规则:
- 如果当前的Promise是未决的,得到的新的Promise是挂起状态(pendding)
- 如果当前的Promise是已决的,会运行相应的后续处理函数,并将后续处理函数的结果(返回值)作为resolved状态数据,应用到新的Promise中;
如果后续处理函数发生错误,则把返回值作为rejected状态数据,应用到新的Promise中。
后续的Promise一定会等到前面的Promise有了后续处理结果后,才会变成已决状态
如果前面的Promise的后续处理,返回的是一个Promise,则返回的新的Promise状态数据和后续处理返回的Promise状态数据保持一致。
const pro1 = new Promise((resolve, reject) => {
resolve(1);
})
const pro2 = pro1.then(data => {
return data * 2; //会返回一个新的promis对象,return 会把数据返回给新的promise对象,当作传输的数据
})
console.log(pro2); // 输出的promis对象是 pendding状态
//因为then(异步函数);里面的代码会放到微队列中等待同步代码执行完,所以因为pro1的thenable函数还没有执行,以至于pro2还是pendding
pro2.then(data => { //这里的data是pro1对象return的数据 这时then可以执行,是因为pro2是第二个进入微队列,比pro1的thenable慢,所以此时pro1和pro2都已经处于fulfilled状态了
console.log(data); //2
console.log(pro2); //fulfilled状态:完成状态
})
细节练习
只要pro1的作业队列函数没有出错(报错),pro2就是thenable状态
const pro1 = new Promise((resolve, reject) => {
throw 2;//抛出错误信息 2
})
const pro2 = pro1.then(data => {
return data * 2;
}, err => {
return err * 3; //6 如果改为 throw err * 3,那么pro2就是调用catchable函数了
})
pro2.then(data => { //当pro1的catchable函数没有发生抛出错误,pro2就会执行thenable函数
console.log(data * 2);
}, err => { //如果抛出错误了就会执行catchable函数
console.log(err * 3);
})
pro2只会对应一个then/catch函数,也就是 = 后面的then/catch
尽管后续pro1有多少个作业队列,都是不影响的
const pro1 = new Promise((resolve, reject) => {
throw 2;
})
const pro2 = pro1.then(data => {
return data * 2;
}, err => {
return err * 3; //6
})
pro1.catch(err => {
return err * 2; //4
})
pro2.then(data => { //当pro1的catchable函数没有发生错误,pro2就会执行thenable函数
console.log(data * 2);//依然是 6 * 2 = 12
}, err => { //如果错误了就会执行catchable函数
console.log(err * 3);
//这里选择err是6,而不是4;因为pro2只会对应一个then/catch函数,也就是 = 后面的then/catch
//所以后面的catch并不会影响pro2的数据
})
因为返回的是promise对象,所以会将promise对象的数据return给pro3,而不是return对象
const pro1 = new Promise((resolve, reject) => {
resolve(1);
})
const pro2 = new Promise((resolve, reject) => {
resolve(2);
})
const pro3 = pro1.then(data => {
return pro2; //此时 因为返回的是promise对象,所以会将promise对象的数据return给pro3,而不是return pro2对象
})
pro3.then(data => {
console.log(data); //2
})
链式写法,下面代码与上面的一样效果:不用创建变量,直接用返回的promise对象调用即可
const pro1 = new Promise((resolve, reject) => {
resolve(1);
})
const pro2 = new Promise((resolve, reject) => {
resolve(2);
})
pro1.then(data => {
return pro2; //此时 因为返回的是promise对象,所以会将promise对象的数据return给pro3,而不是return对象
}).then(data => {
console.log(data); //2
})
始终记住:上一个promise的返回的结果,就是下一个promise的数据
邓哥表白女神案例(练习串联)
function biaobai(god) {
return new Promise((resolve, reject) => {
console.log(`邓哥向${god}表白`);
setTimeout(() => {
if (Math.random() < 0.1) {
resolve(true);
} else {
resolve(false);
}
}, 2000)
})
}
const pro1 = biaobai('女神1').then(resp => {
if (resp) {
console.log('邓哥表白女神1成功');
} else {
return biaobai('女神2')
}
})
const pro2 = pro1.then(resp => {
if (resp === undefined) {//如果是undefined说明 邓哥前面已经表白成功
return;
} else if (resp) {
console.log('邓哥表白女神2成功');
} else {
return biaobai('女神3')
}
})
const pro3 = pro2.then(resp => {
if (resp === undefined) {
return;
} else if (resp) {
console.log('邓哥表白女神3成功');
} else {
console.log('邓哥表白全部失败了');
return;
}
})
通过for循环缩减代码
function biaobai(god) {
return new Promise((resolve, reject) => {
console.log(`邓哥向${god}表白`);
setTimeout(() => {
if (Math.random() < 0.1) {
resolve(true);
} else {
resolve(false);
}
}, 2000)
})
}
const arr = ['女神1', '女神2', '女神3', '女神4'];
let pro = null;
for (let i = 0; i < arr.length; i++) {
if (i == 0) {
pro = biaobai(arr[i]);
}
pro = pro.then(resp => {
if (resp === undefined) {
return;
} else if (resp) {
console.log(`邓哥表白${arr[i]}成功`);
} else {
if (i < arr.length - 1) { //当i为数组长度-1时,已经没有下一个女神了
return biaobai(arr[i + 1])
} else {
console.log('表白全部失败了');
return;
}
}
})
}
promise其他的API
原型成员 (实例成员)
-
then:注册一个后续处理函数,当Promise为resolved状态时运行该函数(其实then也可以注册rejected函数)
-
catch:注册一个后续处理函数,当Promise为rejected状态时运行该函数
-
finally:[ES2018]注册一个后续处理函数(无参),当Promise为已决时运行该函数
(无论是resolved还是rejected,他都会运行,并且它的运行顺序跟注册顺序有关,先注册就先执行)
构造函数成员 (静态成员)
-
resolve(数据):该方法返回一个fulfilled状态的Promise,传递的数据作为状态数据
const pro = new Promise((resolve,reject)=>{ resolve(1);//直接就推向resolved状态 }) //下面代码等同上面的代码 const pro2 = Promise.resolve(1); //如果resolve/reject的括号中传了个promise对象进去,则 const pro3 = Promise.resolve(pro2); //等效于下面代码 const pro4 = pro3; console.log(pro4 === pro3);//true
- 特殊情况:如果传递的数据是Promise,则直接返回传递的Promise对象
-
reject(数据):该方法返回一个rejected状态的Promise,传递的数据作为状态数据
-
all(iterable):这个方法返回一个新的promise对象,该promise对象在iterable参数对象里所有的promise对象都成功的时候才会触发成功,一旦有任何一个iterable里面的promise对象失败则立即触发该promise对象的失败。
这个新的promise对象在触发成功状态以后,会把一个包含iterable里所有promise返回值的数组作为成功回调的返回值,顺序跟iterable的顺序保持一致;
如果这个新的promise对象触发了失败状态,它会把iterable里第一个触发失败的promise对象的错误信息作为它的失败错误信息。
Promise.all方法常被用于处理多个promise对象的状态集合。
const pros = []; for(let i = 0;i < 10; i++){ setTimeout(()=>{ pros.push(new Promise((resolve,reject)=>{ resolve(i); })) },1000) } const pro = Promise.all(pros); pro.then(resp=>{//里面的promise全部resolved,pro才能触发thenable/catchable函数 console.log('全部完成',resp);//全部完成[0,1,2,3,4,5,6,7,8,9] })
- race(iterable):当iterable参数里的任意一个子promise被成功或失败后,父promise马上也会用子promise的成功返回值或失败详情作为参数调用父promise绑定的相应句柄,并返回该promise对象
手写Promise
const myPromise = (function () {
//状态值
const RESOLVED = "RESOLVED",
REJECTED = "REJECTED",
PENDDING = "PENDDING",
//属性
PromiseState = Symbol("PromiseState"),
PromiseValue = Symbol("PromiseValue"),
thenables = Symbol("thenables"),
catchables = Symbol("catchables");
//内部函数
const changeState = Symbol("changeState"),//改变状态
runHandle = Symbol("runHandle"),//执行异步函数
thenValue = Symbol("thenValue");//处理异步函数的返回值
return class myPromise{
constructor(run) {
//状态初始化
this[PromiseState] = PENDDING;
this[PromiseValue] = undefined;
//初始化作用队列
this[thenables] = [];
this[catchables] = [];
//创建resolve、reject
const resolve = data => {
//改变状态
this[changeState](data,RESOLVED,this[thenables])
};
const reject = err => {
//改变状态
this[changeState](err,REJECTED,this[catchables])
}
//调用run,为了实现抛出异常也会改变状态,所以使用try
try {
run(resolve, reject);
} catch (err) {
reject(err);
}
}
/**
* 改变状态
* @param {data} 接收的状态数据
* @param {state} 需要转变的状态
* @param {list} 状态对应的作用队列
*/
[changeState](data,state,list) {
if (this[PromiseState] !== PENDDING) {
return;
}
if (this[PromiseState] === PENDDING) {
this[PromiseState] = state;
this[PromiseValue] = data;
list.forEach(item => item(this[PromiseValue]));
}
}
/**
* 执行异步函数
* @param {state} 函数执行需要的状态
* @param {handle} then/catch传进来的函数
* @param {list} 作用队列
*/
[runHandle](state,handle,list) {
if (this[PromiseState] === state) {
setTimeout(() => {
handle(this[PromiseValue]);
},0)
}else {
list.push(handle);
}
}
/**
* 处理then函数的返回值
* @param {thenable} then的第一个参数
* @param {catchable} then的第二个参数
*/
[thenValue](thenable, catchable) {
//检验result值
function testResult(result, resolve) {
if (result instanceof myPromise) {
//递归调用then,得到myPromise对象的数据
result.then(d => {//将result的状态数据 赋值给 返回的promise状态数据
resolve(d);
}, e => {
resolve(e);
})
} else {//否则将promise的状态数据等于thenable的返回数据
resolve(result);
}
}
return new myPromise((resolve, reject) => {
//thenable
this[runHandle](RESOLVED, data => {
try {
if (typeof thenable !== "function") {
return;
}
const result = thenable(data); //有可能抛出异常,抛出异常执行catch代码
testResult(result, resolve);
} catch (err) {
reject(err);
}
}, this[thenables])
//catchable
this[runHandle](REJECTED, data => {
try {
if (typeof catchable !== "function") {
return;
}
const result = catchable(data);
testResult(result, resolve);
} catch (err) {
reject(err);
}
}, this[catchables])
})
}
then(thenable,catchable) {
return this[thenValue](thenable, catchable);
}
catch(catchable) {
return this[thenValue](undefined, catchable);
}
}
})()
async 和 await
async 和 await 是 ES2016 新增两个关键字,它们借鉴了 ES2015 中生成器在实际开发中的应用,目的是简化 Promise api 的使用,并非是替代 Promise。
async
目的是简化在函数的返回值中对Promise的创建
async 用于修饰函数(无论是函数字面量还是函数表达式),放置在函数最开始的位置,被修饰函数的返回结果一定是 Promise 对象
async function test(){
console.log(1);
return 2;//如果没有return 那么最后Promise对象得到的值就是undefined
// throw 2 得到的promise对象就是rejected状态
}
//等效于
function test(){
return new Promise((resolve, reject)=>{
console.log(1);
resolve(2);
})
}
注意:在async函数中不能在定时器中return数据,因为这样return的不是async函数,而是定时器中的函数return
async function test1(){
setTimeout(()=>{
return 2;//这样并不能实现resolve效果 return的是定时器里面的箭头函数
})
}
await
await关键字必须出现在async函数中!!!!
await用在某个表达式之前,如果表达式是一个Promise,则得到的是thenable中的状态数据。
async function test1(){
console.log(1);
return 2;
}
async function test2(){//返回一个promise对象 值是undefined 状态fulfilled
const result = await test1();
console.log(result);
//如果这里 return 2; 等待await处于已决阶段之后,返回的promise对象 值是2 状态fulfilled
}
test2();
等效于
function test1(){
return new Promise((resolve, reject)=>{
console.log(1);
resolve(2);
})
}
function test2(){
return new Promise((resolve, reject)=>{
test1().then(data => {//当test1()成功后把data传给result
const result = data;
console.log(result);
resolve();
})
})
}
test2();
面试题可能会考:如果await的表达式不是Promise,则会将其使用Promise.resolve包装后按照规则运行
async function test(){
const a = await 1;
console.log(a);
}
test();
console.log(2);
//先输出 2
//再输出 1
等同于下面代码
function test(){
return new Promise((resolve,reject)=>{
Promise.resolve(1).then(data=>{
const a = data;
console.log(a);
})
})
}
test();
console.log(2);
async和await的细节注意
await等待一个promise运行完后,才能执行下面的代码,不然会一直等待
function time() {//由于async中使用定时器无法实现resolve所以才不使用async模拟
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 5000);
})
}
async function test() {
console.log(1);
const a = await time();
console.log(2);
}
test();
// 先输出1
//等待5秒后 输出2
输出顺序,await后面的代码相当于是then函数里面的thenable函数,是异步函数,在微队列中等待
async function time() {
console.log(1);//同步代码
}
async function test() {
const a = await time();//相当于在then中执行的代码 也就是下面的代码被放到了微队列中等待同步代码执行完毕
console.log(2);
console.log(time());
}
// test();
console.log(test());
// 输出顺序
1
Promise {<pending>}
2
1
Promise {<fulfilled>: undefined}
async可以修饰匿名函数,如下面立即执行函数
async function test1(){//也可以使用箭头函数
console.log(1);
return 2;
}
(async function(){
const a = await test1();
console.log(a);//2
}())
成功状态和错误状态的数据传输(需要使用try,不然抛出错误后,后面的代码就无法执行了)
async function test(){
if(Math.random() < 0.5){
return 1;//resolve
}else{
throw 2;//reject
}
}
(async()=>{
try{
const a = await test();
console.log('成功状态:'+a);//如果这里是抛出错误,则执行catch里的代码
}catch(err){
console.log('错误状态:'+err);
}
}());
// 输出 成功状态:1
//或 错误状态:2
promise改造定时器
实现指定时间后才会推向已决状态
直接用setTimeout或setInterval是无法实现的
//promise改造定时器
function delay(duration) {//等待duration时间后才会推向已决状态
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, duration);
})
}
async function test() {
console.log(1);
await delay(2000); //等待2秒钟
console.log(2); //2秒后输出2
}
test();
FetchAPI的概述
XMLHttpRequest的问题
- 所有的功能全部集中在同一个对象上,容易书写出混乱不易维护的代码
- 采用传统的事件驱动模式,无法适配新的 Promise Api
Fetch Api 的特点
-
并非取代 AJAX,而是对 AJAX 传统 API 的改进
ajax是一个浏览器向服务器请求的一个概念,可以理解为一个标准,并非取代ajax 只不过我们之前使用的是传统方式的ajax,而fetch是改进过后的ajax使用方式
-
精细的功能分割:头部信息、请求信息、响应信息等均分布到不同的对象,更利于处理各种复杂的 AJAX 场景
-
使用 Promise Api,更利于异步代码的书写
-
Fetch Api 并非 ES6 的内容,属于 HTML5 新增的 Web Api
-
需要掌握网络通信的知识
Fetch基本使用
语法
函数有两个参数:
1.必填,字符串,请求地址
2.选填,对象,请求配置
const url = 'http://101.132.72.36:5100/api/local';
const config = {
method:'POST',
headers:{
Content-Type:'application/json',//请求的内容是 json格式数据
},
body:`{'a':'1'}`
}
fetch(url,config);//这样就能发送请求了
请求配置对象
请求配置对象
- method:字符串,请求方法,默认值GET
- headers:对象,请求头信息
- body: 请求体的内容,必须匹配请求头中的 Content-Type(比如需要post请求时,请求参数是放在请求体中的,就需要配置在body上)
- mode:字符串,请求模式
- cors:默认值,配置为该值,会在请求头中加入 origin 和 referer
- no-cors:配置为该值,不会在请求头中加入 origin 和 referer,跨域的时候可能会出现问题
- same-origin:指示请求必须在同一个域中发生,如果请求其他域,则会报错
- credentials: 如何携带凭据(cookie)
- omit:默认值,不携带cookie
- same-origin:请求同源地址时携带cookie
- include:请求任何地址都携带cookie
- cache:配置缓存模式
- default: 表示fetch请求之前将检查下http的缓存.
- no-store: 表示fetch请求将完全忽略http缓存的存在. 这意味着请求之前将不再检查下http的缓存, 拿到响应后, 它也不会更新http缓存.
- no-cache: 如果存在缓存, 那么fetch将发送一个条件查询request和一个正常的request, 拿到响应后, 它会更新http缓存.
- reload: 表示fetch请求之前将忽略http缓存的存在, 但是请求拿到响应后, 它将主动更新http缓存.
- force-cache: 表示fetch请求不顾一切的依赖缓存, 即使缓存过期了, 它依然从缓存中读取. 除非没有任何缓存, 那么它将发送一个正常的request.
- only-if-cached: 表示fetch请求不顾一切的依赖缓存, 即使缓存过期了, 它依然从缓存中读取. 如果没有缓存, 它将抛出网络错误(该设置只在mode为”same-origin”时有效).
返回值
fetch 函数返回一个 Promise 对象
-
当收到服务器的返回结果后,Promise 进入resolved状态,状态数据为 Response 对象
-
当网络发生错误(或其他导致无法完成交互的错误)时,Promise 进入 rejected 状态,状态数据为错误信息
const url = 'http://101.132.72.36:5100/api/local'; console.log(fetch(url));
Response对象
-
ok:boolean,当响应消息码在200~299之间时为true,其他为false
-
status:number,响应的状态码
-
text():用于处理文本格式的 Ajax 响应。它从响应中获取文本流,将其读完,然后返回一个被解决为 string 对象的 Promise。
-
blob():用于处理二进制文件格式(比如图片或者电子表格)的 Ajax 响应。它读取文件的原始数据,一旦读取完整个文件,就返回一个被解决为 blob 对象的 Promise。
-
json():用于处理 JSON 格式的 Ajax 的响应。它将 JSON 数据流转换为一个被解决为 JavaScript 对象的promise。
-
redirect():可以用于重定向到另一个 URL。它会创建一个新的 Promise,以解决来自重定向的 URL 的响应。
test( ):解析成文本
(async() => {
const url = 'http://101.132.72.36:5100/api/local';
const pro = await fetch(url); //返回Response对象
const text = await pro.text();
console.log(text); //所需要的数据
})();
json():解析成json格式的对象
(async() => {
const url = 'http://101.132.72.36:5100/api/local';
const pro = await fetch(url); //返回Response对象
const data = await pro.json();
console.log(data); //所需要的json格式对象
})();
Request 对象
除了使用基本的fetch方法,还可以通过创建一个Request对象来完成请求(实际上,fetch的内部会帮你创建一个Request对象)
new Request(url地址, 配置)
注意点:
尽量保证每次请求都是一个新的Request对象
用法
(async() => {
const url = 'http://101.132.72.36:5100/api/local';
const req = new Request(url);
const pro = await fetch(req); //返回Response对象
const data = await pro.json();
console.log(data); //所需要的json格式对象
})();
Response对象
有时候可能不想用url调试代码,
就可以创建一个response对象进行测试
就可以使用**new Response(响应体,属性配置)**创建
(async() => {
const resp = new Response(`[
{"id":1,"name":"清远"},
{"id":1,"name":"清远"}
]`, {
ok: true,
status: 200
})
const data = await resp.json();
console.log(data); //所需要的json格式数据
})();
Headers对象
在fetch方法中,配置对象里面其实也可以配置headers
其实,它运行时,实际上是创建了一个Headers对象,然后再放进配置对象中进行请求的
所以代码可以写成这样
(async() => {
const url = 'http://101.132.72.36:5100/api/local';
const head = new Headers({
a: 1,
b: 2
});
const pro = await fetch(url, {
headers: head //这样请求头里 就会有 {a:1,b:2}
}); //返回Response对象
const data = await pro.json();
console.log(data); //所需要的json格式对象
})();
Headers对象中的方法:
- has(key):检查请求头中是否存在指定的key值
- get(key): 得到请求头中对应的key值(如果key不存在,则返回null)
- set(key, value):修改对应的键值对(如果修改的key不存在,则会新创建一个key)
- append(key, value):添加对应的键值对
- keys(): 得到所有的请求头键的集合
- values(): 得到所有的请求头中的值的集合
- entries(): 得到所有请求头中的键值对的集合
文件上传
流程:
- 客户端将文件数据发送给服务器
- 服务器保存上传的文件数据到服务器端
- 服务器响应给客户端一个文件访问地址
测试地址:http://101.132.72.36:5100/api/upload 键的名称(表单域名称):imagefile
请求方法:POST
请求的表单格式:multipart/form-data 请求体中必须包含一个键值对,键的名称是服务器要求的名称(要和服务器的名称相对应),值是文件数据(input对象.files[0])
HTML5中,JS仍然无法随意的获取文件数据,但是可以获取到input元素中,被用户选中的文件数据 可以利用HTML5提供的FormData构造函数来创建请求体
可以获取到input元素中被选中的文件数据,如下:
<body>
<input type="file" id="avatar">
<!--上传文件-->
<script>
function upload() {
const inp = document.getElementById('avatar');
console.log(inp.files);
}
</script>
</body>
上传图片数据
<body>
<input type="file" id="avatar">
<!--上传文件-->
<script>
async function upload() {
const inp = document.getElementById('avatar');
if (!inp.files) {
alert('请选择文件上传');
return;
}
const formData = new FormData(); //创建请求体
//添加键值
formData.append("imagefile", inp.files[0]);
//然后就是发送请求
const url = "http://101.132.72.36:5100/api/upload";
const pro = await fetch(url, {
method: 'POST',
body: formData, //请求体放创建好的就行
});
//转为json格式对象
const result = await pro.json();
console.log(result);//输出图片地址
//{path: "http://images.yuanjin.tech/Fgqr9auiMtyS0sUc57GgYJ8TUdva"}
}
</script>
</body>
迭代器
背景知识
- 什么是迭代?
从一个数据集合中按照一定的顺序,不断取出数据的过程
- 迭代和遍历的区别?
迭代强调的是依次取数据,并不保证取多少,也不保证把所有的数据取完
遍历强调的是要把整个数据依次全部取出
- 迭代器
对迭代过程的封装,在不同的语言中有不同的表现形式,通常为对象
- 迭代模式
一种设计模式,用于统一迭代过程,并规范了迭代器规格:
- 迭代器应该具有得到下一个数据的能力
- 迭代器应该具有判断是否还有后续数据的能力
JS中的迭代器
JS规定,如果一个对象具有next方法,并且该方法返回一个对象,该对象的格式如下:
{value: 值, done: 是否迭代完成}
则认为该对象是一个迭代器
如:
const obj = {//obj就是迭代器 符合条件的都叫迭代器
next() {
return {
value: xxx,
done: xx
}
}
}
含义:
- next方法:用于得到下一个数据
- 返回的对象
- value:下一个数据的值
- done:boolean,是否迭代完成(true/false)
代码实现迭代器原理
const arr = [1,2,3,4,5];
const iterator = {
i: 0, //表示元素当前数组下标
next() {
return {
value: arr[this.i++], //value = this[i],然后i++
done: this.i >= arr.length // 如果i++之后超出了最大下标,则表示下标完成
}
}
}
//编写完迭代器之后,如果需要数据,只需要调用一次函数即可拿到下一个数据
let data = iterator.next();//得到第一个数据
while (!data.done) {//如果done不为true ,就一直执行下面代码
console.log(data.value);
data = iterator.next();//调用next()得到下一个数据
}
console.log('迭代完成');//跳出循环后输出 迭代完成
通过迭代器可以实现将数组和获取数据者分离开,获取数据者完全不需要见到数组
**简单理解:**迭代器就是一个厂库(数组)的管家,而获取数据者只需要向管家拿数据即可,至于厂库里面到底怎么运作的不需要管
迭代器创建函数
一个返回迭代器的函数
//迭代器创建函数 iterator creator
function createIterator(arr) {
let i = 0; //表示元素当前数组下标
return { //返回迭代器
next() {
return {
value: arr[i++], //value = this[i],然后i++
done: i >= arr.length // 如果i++之后超出了最大下标,则表示下标完成
}
}
}
}
迭代器的优势之一
当需要对一个无限长的数组进行遍历或者获取数据时,使用迭代器是最好的方式
它不需要知道数组有多长,不需要把整个数组装进去遍历
而如果使用常规遍历,则就需要把整个数组都得装进去进行遍历,可能会耗费很大的性能
迭代器的核心思想
就相当于一个机器
只会无脑地一直取下一个,并判断下一个是否还有值
迭代器协议
可迭代协议
ES6规定,如果一个对象具有知名符号属性Symbol.iterator
,并且属性值是一个迭代器创建函数,则该对象是可迭代的(iterable)
比如
const obj = {
[Symbol.iterator](){
return {
next(){
return{
value:1,
done:false
}
}
}
}
}
思考
如何知道这个对象是不是可迭代的?
1.查看这个对象是否含有Symbol(Symbol.iterator)属性,并且是一个方法
2.该方法返回一个迭代器对象
3.迭代器对象要求含有属性 value:当前值,done:是否迭代完成
如何遍历一个可迭代对象?
const arr = [1, 2, 3, 4, 5, 6, 7];
const iterator = arr[Symbol.iterator](); //调用数组的迭代器创建函数,以获取数组的迭代器
let iterable = iterator.next(); //获取迭代器返回的第一个对象
while (!iterable.done) {
const item = iterable.value;
console.log(item);
//下一次迭代
iterable = iterator.next();
}
console.log('迭代完成');
在ES6中就把所有数组都变成了可迭代对象,里面也有Symbol(Symbol.iterator)属性
for-of 循环
for-of 循环用于遍历可迭代对象,格式如下
所用到的原理就是上面所写的代码
//迭代完成后循环结束
for(const item in iterable){
//iterable:可迭代对象
//item:每次迭代得到的数据(相当于iterable.value)
}
通过自定义迭代器,可以改变for-of的item返回的值
//自定义编写Symbol(Symbol.iterator),实现for of遍历输出对象的属性:值
const obj = {
a: 1,
b: 2,
[Symbol.iterator]() {
let keys = Object.keys(this);//获取对象的所有属性名,返回一个数组
let i = 0;
return {
next: () => {
const key = keys[i];//属性名
const keyValue = this[keys[i]];//属性值
i++; //判定前i已经是1了,所以是>keys.length 也就是3的时候变成true
return {
value: {
[key]: keyValue
},
done: i > keys.length ? true : false
}
}
}
}
}
for (const item of obj) {
console.log(item);
}
展开运算符与可迭代对象
展开运算符可以作用于可迭代对象,这样,就可以轻松的将可迭代对象转换为数组。
实际上展开运算符展开的是可迭代对象
只不过展开运算之后,返回的是一个数组(即使对可迭代对象进行展开)
迭代器和可迭代对象的关系
- **可迭代对象:**如果对象中含有Symbol(Symbol.iterator)属性方法,则表示这是一个可迭代对象
- **迭代器:**是Symbol(Symbol.iterator)属性方法执行返回的对象,对象中含有next()方法,next()可以返回一个对象,里面有value、done属性;
生成器
实际上,生成器可以理解为是操作迭代器的一个语法糖
什么是生成器?
- 生成器是一个通过构造函数Generator创建的对象(但是,我们是不能直接new构造函数创建生成器,因为Generator只给内部使用)
- 生成器既是一个迭代器,也是一个可迭代对象(说明:生成器同时含有**next()方法以及Symbol(Symbol.iterator)**方法)
注意:生成器实际上并没有next()和Symbol(Symbol.iterator)这两个属性,但是它的原型上有这两个属性,所以可以正常调用;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kCOrgZpe-1627093369331)(C:\Users\26704\AppData\Roaming\Typora\typora-user-images\image-20201127151550030.png)]
如何创建生成器?
- 生成器的创建,必须使用生成器函数(Generator Function)
- 生成器函数的使用方式:只需要在函数名前面或function关键字后面加上一个 ***** ,就是表示生成器函数,比如:
该函数一定会返回一个生成器(即使函数里面没有任何东西)
所以:函数不能同时添加 async、* 两个关键字,async返回promise对象,* 返回生成器对象
//写法一:
function *method(){};
//写法二:
function* method(){};
注意:
调用生成器函数,作用只有一个:就是创建生成器
所以调用生成器函数时,即使生成器函数里面有多少代码,都不会执行
生成器函数如何执行?
实际上,生成器就是为了简便迭代器的代码编写
- 生成器函数的内部:是为了给生成器的每一次迭代提供数据的
所以:生成器函数的内部的代码,只有进行迭代才会执行
每次调用生成器的next方法,将导致生成器函数运行到下一个yield关键字位置,直到找不到yield关键字后
返回{value : undefined , done : true}
-
yield:(读音:一偶的)是一个关键字,该关键字只能在生成器函数内部使用,表达:“产生”一个迭代数据的意思。
-
调用生成器迭代的方式:生成器对象.next()
运行过程如下:
function* method() {
console.log('第一次运行');
yield 1; //产生一个{value : 1,done:false/true取决于后面是否还存在yield关键字}的对象,并返回
console.log('第二次运行');
yield 2;
console.log('第三次运行');
//运行到这的时候,因为后面没有yield了,所以产生的数据是{value:undefined,done:true},并返回
};
const generator = method();
//调用生成器迭代的方式:生成器对象.next()
生成器的小练习
迭代数组
通过生成器,就能把生成器函数的内部变得可控,便捷于迭代数据
const arr = [1, 2, 3, 4, 5];
function* getArr(arr) {
for (const item of arr) { //此处相当于生成arr.length个yield关键字,并且产生的value = item
yield item;
}
}
const props = getArr(arr); //返回一个生成器对象
console.log(props.next()); //{value: 1, done: false}
console.log(props.next()); //{value: 2, done: false}
console.log(props.next()); //{value: 3, done: false}
console.log(props.next()); //{value: 4, done: false}
console.log(props.next()); //{value: 5, done: false}
console.log(props.next()); //{value: undefined, done: true}
斐波拉契数列迭代
function* getFeiboGenerator() {
let prev1 = 1,//前一位
prev2 = 1,//前二位
n = 1;
while (true) { //斐波拉契数列是无限的,所以使用死循环
if (n <= 2) {
yield 1;
} else {
const result = prev1 + prev2;
yield result;
prev2 = prev1;
prev1 = result;
}
n++;
}
}
const feibo = getFeiboGenerator();
console.log(feibo.next()); //{value: 1, done: false}
console.log(feibo.next()); //{value: 1, done: false}
console.log(feibo.next()); //{value: 2, done: false}
console.log(feibo.next()); //{value: 3, done: false}
console.log(feibo.next()); //{value: 5, done: false}
生成器需要注意的细节
有哪些需要注意的细节?
-
生成器函数可以有返回值,返回值出现在第一次done为true时的value属性中
**可以理解为:**当程序读到return时,就会把返回对象中的done变成true,并把值赋给value(也就是把迭代结束了)
导致 return 后面如果还遇到yield关键字,则返回的数据都是 {value : undefined , done : true}
function* method() { console.log('第一次运行'); yield 1; console.log('第二次运行'); yield 2; console.log('第三次运行'); return 10; } const generator = method(); console.log(generator.next()); console.log(generator.next()); console.log(generator.next());
-
调用生成器的next方法时,可以传递参数,传递的参数会交给yield表达式的返回值
function* method() { let info = yield 1; //第一次next没传参数时,返回{value: 1, done: false},并且程序停在这 console.log(info); //第二次next(5)传了参数后,就会把(yield 1)的返回值变为参数值5,所以此时的info = 5,并输出5 info = yield 2 + info; // info = yield 2 + 5;返回{value: 7, done: false},并且程序停在这 console.log(info); //第三次next(1)传了参数后,把(yield 2 + 5)变成参数值1,所以此时的info = 1,并输出1 //由于查找不到yield了,所以返回{value: undefined, done: true} } const generator = method();
-
所以第一次调用next方法时,传参没有任何意义
-
在生成器函数内部,可以调用其他生成器函数,但是要注意加上*号
function* t1() { yield 'a'; yield 'b'; } function* test() { yield* t1(); //这样调用可以理解为在这里加入了以下代码: //yield 'a'; //yield 'b'; yield 1; yield 2; } const generator = test(); let iterable = generator.next(); while (!iterable.done) { console.log(iterable.value); iterable = generator.next(); }
生成器的其他API
- return方法:调用该方法,可以提前结束生成器函数,从而提前让整个迭代过程结束
- throw方法:调用该方法,可以在生成器中产生一个错误
使用生成器模拟异步函数处理
function* task() {
const d = yield 1;
console.log(d); //1
const resp = yield fetch('http://101.132.72.36:5100/api/local');//返回promise对象
//然后通过next()传值,把 yield fetch得到的数据转为data
result = yield resp.json();//当执行该行代码时,resp已经是请求得到的data了
console.log(result);
}
run(task);
function run(generatorFunc) {
const generator = generatorFunc();
let result = generator.next(); //启动任务 并获得第一个生成器对象数据{value:1,done:false}
//判断数据
handleResult();
function handleResult() {
if (result.done) { //直到result.done为true时,表示任务完成,结束操作
return;
}
if (typeof(result.value.then) === 'function') { //返回的value是一个promise对象
//promise对象数据
result.value.then(data => { //当fetch请求成功后,返回promise对象的data数据
result = generator.next(data); //此处的data赋值给了resp,并继续迭代下一个数据
handleResult(); //继续进行判断
}, err => {
throw err;
})
} else {
//普通数据
result = generator.next(result.value); //把value赋值给result,也就是1,并继续迭代下一个数据
handleResult(); //继续进行判断
}
}
}
更多的集合类型
set集合
一直以来,JS只能使用数组和对象来保存多个数据,缺乏像其他语言那样拥有丰富的集合类型。因此,ES6新增了两种集合类型(set 和 map),用于在不同的场景中发挥作用。
set用于存放不重复的数据
- 如何创建set集合
new Set(); //创建一个没有任何内容的set集合
new Set(iterable); //创建一个具有初始内容的set集合,内容来自于可迭代对象每一次迭代的结果
如
const a = new Set();
console.log(a);//{}
const b = new Set([1,2,3,4]);
console.log(b);//{1, 2, 3, 4}
//如果是一些重复的数据,则Set会自动去重
const c = new Set([1,2,3,1,2,4,4,5]);
console.log(c);//{1, 2, 3, 4, 5}
const d = new Set('aabbcssddss');
console.log(d);//{"a", "b", "c", "s", "d"}
- 如何对set集合进行后续操作
- add(数据): 添加一个数据到set集合末尾,如果数据已存在,则不进行任何操作
- set使用Object.is的方式判断两个数据是否相同,但是,针对+0和-0,set认为是相等
- has(数据): 判断set中是否存在对应的数据
- delete(数据):删除匹配的数据,返回是否删除成功(注意:括号里面不是放下标,是数据值)
- clear():清空整个set集合
- size: 获取set集合中的元素数量,只读属性,无法重新赋值
- 如何与数组进行相互转换
const s = new Set([x,x,x,x,x]);
// set本身也是一个可迭代对象,每次迭代的结果就是每一项的值
const arr = [...s];
- 如何遍历
1). 使用for-of循环
2). 使用set中的实例方法forEach
注意:set集合中不存在下标,因此forEach中的回调的第二个参数和第一个参数是一致的,均表示set中的每一项
set应用
求两个数组的并集、交集、差集
const arr1 = [11, 22, 33, 44, 55, 55, 22, 77];
const arr2 = [33, 88, 12, 22, 99, 99, 66, 55];
console.log('arr1', arr1);
console.log('arr2', arr2);
//并集
console.log('并集', [...new Set([...new Set(arr1), ...new Set(arr2)])]);
console.log('并集', [...new Set(arr1.concat(arr2))]);
//交集
const cross = [...new Set(arr1)].filter(item => arr2.indexOf(item) >= 0);
console.log('交集', cross);
//差集(在并集中找出arr1独有的和arr2独有的元素或用 并集-交集)
console.log('差集', [...new Set([...new Set(arr1), ...new Set(arr2)])].filter(item => arr1.indexOf(item) >= 0 && arr2.indexOf(item) < 0 || arr1.indexOf(item) < 0 && arr2.indexOf(item) >= 0));
console.log('差集', [...new Set([...new Set(arr1), ...new Set(arr2)])].filter(item => cross.indexOf(item) < 0));
手写Set()
class MySet {
constructor(iterable = []) { //如果不传值,默认[]
//先判断是否为可迭代对象
if (typeof iterable[Symbol.iterator] !== 'function') {
throw new TypeError(iterable + '不是可迭代对象');
}
this._datas = []; //只能借助数组来模仿Set()的返回值
for (const item of iterable) {
this.add(item);
}
// for (const item of this._datas) {
// }
}
add(data) {
if (!this.has(data)) {
this._datas.push(data);
}
}
has(data) {
for (const item of this._datas) {
if (this.isEqual(data, item)) { //如果数值相同,则返回true
return true;
}
}
return false;
}
isEqual(data, target) {
if (data == 0 && target == 0) {
return true;
}
return Object.is(data, target);
};
[Symbol.iterator] = function*() { //生成器函数
for (const item of this._datas) {
yield item;
}
}
forEach(func) {
if (typeof func !== 'function') {
return;
}
for (const item of this._datas) {
func(item, item, this);
}
}
}
const a = new MySet([1, 2, 3, 3, 4]);
console.log(a);
map集合
键值对(key value pair)数据集合的特点:键不可重复
map集合专门用于存储多个键值对数据。
在map出现之前,我们使用的是对象的方式来存储键值对,键是属性名,值是属性值。
使用对象存储有以下问题:
- 键名只能是字符串
- 获取数据的数量不方便
- 键名容易跟原型上的名称冲突
何时使用对象,何时使用集合
- 对象:当需要存储的信息可以构成一个完整的整体,并且基本不会改变数据的时候,使用对象。(如:用户信息)
- map集合:当存储的数据需要较多次的修改,即使少了一个或多个数据,也并不影响后续操作时,使用集合(如:请求头信息)
如何创建map
new Map();//创建了一个空的map集合
new Map(iterable); //创建一个具有初始内容的map,初始内容来自于可迭代对象每一次迭代的结果,但是,它要求每一次迭代的结果必须是一个长度为2的数组,数组第一项表示键,数组的第二项表示值
如
const a = new Map([
['a', 1],
['b', 2]
]);
console.log(a);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yVEZlVDF-1627093369333)(C:\Users\26704\AppData\Roaming\Typora\typora-user-images\image-20201128104927300.png)]
如何进行后续操作
- size:只读属性,获取当前map中键的数量
- set(键, 值):设置一个键值对,键和值可以是任何类型
- 如果键不存在,则添加一项
- 如果键已存在,则修改它的值
- 比较键的方式和set相同
- get(键): 根据一个键得到对应的值
- has(键):判断某个键是否存在
- delete(键):删除指定的键
- clear(): 清空map
和数组互相转换
和set一样(拓展运算符)
- 遍历
- for-of,每次迭代得到的是一个长度为2的数组
- forEach,通过回调函数遍历
- 参数1:每一项的值
- 参数2:每一项的键
- 参数3:map本身
手写map集合
class MyMap {
constructor(iterable = []) {
if (typeof iterable[Symbol.iterator] !== 'function') { //传进来的数据不是可迭代对象
throw new TypeError(`${iterable}不是可迭代对象`);
}
for (const item of iterable) {
if (typeof item[Symbol.iterator] !== 'function') {
throw new TypeError(`${item}不是可迭代对象`);
}
}
this._datas = [];
for (const item of iterable) {
this.set(item[0], item[1]); //{key:item[0],value:[item1]}
}
}
get size() {
return this._datas.length;
}
_getObj(key) { //根据key返回对应的对象
for (const item of this._datas) {
if (this._isEqual(item.key, key)) {//判断是否已经存在
return item; //返回对象
}
}
}
set(key, value) {
const obj = this._getObj(key);
if (obj) { //如果里面已经有key
//修改
obj.value = value;
} else {
this._datas.push({
key,
value
});
}
}
get(key) {
return this._getObj(key).value;
}
has(key) {
for (const item of this._datas) {
if (this._isEqual(item.key, key)) { //如果true,证明里面有相同的key
return true;
}
}
return false;
}
delete(key) {
for (let i = 0; i < this._datas.length; i++) {
const element = this._datas[i];
if (element.key === key) {
this._datas.splice(i, 1);
return true;
}
}
return false;
}
clear() {
this._datas.length = 0;
}
_isEqual(data1, data2) {
if (data1 == 0 && data2 == 0) {
return true;
}
return Object.is(data1, data2);
}
[Symbol.iterator] = function*() { //生成器函数,
for (const {
key,
value
}
of this._datas) { //{key,value}是将{key:xxx,value:xxx}解构
yield [key, value]; //返回一个{value:[key,value],done:false};数据
}
}
forEach(func) {
for (let i = 0; i < this._datas.length; i++) {
const element = this._datas[i];
func(element, i, this._datas);
}
}
}
const a = new MyMap([
['a', 1],
['b', 2],
['c', 3],
['b', 10],
['a', 20]
]);
console.log(a);
WeakSet和WeakMap
WeakSet
使用该集合,可以实现和set一样的功能,不同的是:
- 它内部存储的对象地址不会影响垃圾回收(set集合是会阻碍系统垃圾回收的,即使该地址没有变量使用了,也不会被回收)
- 只能添加对象(因为他就是用来监测对象引用的)
- 不能遍历(不是可迭代的对象)、没有size属性、没有forEach方法
垃圾回收是指:
如果该地址已经没有变量使用它了,那么系统就会把他当作垃圾回收起来。
WeakMap
类似于map的集合,不同的是:
- 它的键存储的地址不会影响垃圾回收
- 它的键只能是对象
- 不能遍历(不是可迭代的对象)、没有size属性、没有forEach方法
回顾属性描述符(非ES6内容)
Property Descriptor
Property Descriptor 属性描述符 是一个普通对象,用于描述一个属性的相关信息
通过**Object.getOwnPropertyDescriptor(对象, 属性名)**可以得到一个对象的某个属性的属性描述符
-
configurable(默认false):该属性的描述符是否可以修改(属性描述符还有一些其他的相关信息,也是用于配置属性)
-
enumerable(默认false):该属性是否可以被枚举(也就是:是否可以使用for-in枚举)
-------------------------------------------------数据描述符具有以下可选键值
-
value(可选,默认undefined):属性值
-
writable(可选,默认false):该属性是否可以被重新赋值(也就是指value是否可以修改)
-------------------------------------------------存取描述符具有以下可选键值
-
set(可选,默认undefined):属性的 setter 函数。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值)。(如:对象.属性名=值)
-
get(可选,默认undefined):属性的 getter 函数。当访问该属性时,会调用此函数。执行时不传入任何参数,该函数的返回值会被用作属性的值。
**Object.getOwnPropertyDescriptors(对象)**可以得到某个对象的所有属性描述符(这里的跟上面的方法多了个s)
Object.defineProperty
如果需要为某个对象添加属性时 或 修改属性时, 配置其属性描述符,可以使用下面的代码:
Object.defineProperty(对象, 属性名, 描述符);
Object.defineProperties(对象, 多个属性的描述符)
如:
Object.defineProperty()
const obj = {};
Object.defineProperty(obj,'a',{
value:1,
configurable:true,//描述符可修改
enumerable:true,//可枚举
writable:true//value可修改
})
Object.defineProperties()
const obj = {};
Object.defineProperty(obj,{
a:{//属性a
value:1,
configurable:true,
enumerable:true,
writable:true
},
b:{//属性b
value:1,
configurable:true,
enumerable:true,
writable:true
},
})
存取器属性
属性描述符中,如果配置了 get 和 set 中的任何一个,则该属性,不再是一个普通属性,而变成了存取器属性。
get 和 set配置均为函数,
如果一个属性是存取器属性,则读取该属性时,会运行get方法,将get方法得到的返回值作为属性值;
如果给该属性赋值,则会运行set方法。
const obj = {};
Object.defineProperty(obj, 'a', {
get() {
console.log('属性a被访问了');
},
set(val) {
console.log('属性a被设置了',val);
}
})
console.log(obj.a); //undefined,因为get()没有return数据
console.log(obj.a = 1); //1,把等号右边的值赋给a,并且执行set(1)
//控制台输出:
//属性a被访问了
//undefined
//属性a被设置了1
//1
obj.a = obj.a + 1;// 调用set(obj.a+1); obj.a调用get()返回undefined,undefined+1 = NaN,此时的obj.a=NaN;
console.log(obj.a)//再次调用get() 返回undefined
控制台输出:
//属性a被访问了
//属性a被设置了NaN
//属性a被访问了
//undefined
设置set()和get() 让它变成一个正常可以赋值可以访问的属性
const obj = {};
Object.defineProperty(obj, 'a', {
get() {
console.log('属性a被访问了');
return obj._a;
},
set(val) {
console.log('属性a被设置了' + val);
obj._a = val;
}
})
obj.a = 10;
console.log(obj.a);//10
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6B5Wj6Nm-1627093369334)(C:\Users\26704\AppData\Roaming\Typora\typora-user-images\image-20201128162032596.png)]
存取器属性最大的意义,在于可以控制属性的读取和赋值。
注意
当属性配置了value、writable
那么就不能再配置set()、get()
因为value本身就是表示内存空间的数据,writable就是指这个内存空间能不能修改
而设置set()、get()就是变成运行函数了,不占用内存空间的
两者相互矛盾
如果配置了set()、get(),再去配置value、writable,则会报错
Reflect 反射
Reflect是什么?
Reflect是一个内置的JS对象,它提供了一系列方法,可以让开发者通过调用这些方法,访问一些JS底层功能
由于它类似于其他语言的反射,因此取名为Reflect
它可以做什么?
使用Reflect可以实现诸如 属性的赋值与取值、调用普通函数、调用构造函数、判断属性是否存在与对象中 等等功能
这些功能不是已经存在了吗?为什么还需要用Reflect实现一次?
有一个重要的理念,在ES5就被提出:减少魔法、让代码更加纯粹
这种理念很大程度上是受到函数式编程的影响
ES6进一步贯彻了这种理念,它认为,对属性内存的控制、原型链的修改、函数的调用等等,这些都属于底层实现,属于一种魔法,因此,需要将它们提取出来,形成一个正常的API,并高度聚合到某个对象中,于是,就造就了Reflect对象
因此,你可以看到Reflect对象中有很多的API都可以使用过去的某种语法或其他API实现。
Reflect对象提供的API
- Reflect.set(target, propertyKey, value): 设置对象target的属性propertyKey的值为value,等同于给对象的属性赋值
- Reflect.get(target, propertyKey): 读取对象target的属性propertyKey,等同于读取对象的属性值
- Reflect.apply(target, thisArgument, argumentsList):调用一个指定的函数,并绑定this和参数列表。等同于函数调用
- Reflect.deleteProperty(target, propertyKey):删除一个对象的属性
- Reflect.defineProperty(target, propertyKey, attributes):类似于Object.defineProperty,不同的是如果配置出现问题,返回false而不是报错
- Reflect.construct(target, argumentsList):用构造函数的方式创建一个对象
- Reflect.has(target, propertyKey): 判断一个对象是否拥有一个属性(类似 in关键字: ‘a’ in obj ,有的话返回true)
- 其他API:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
Proxy 代理
代理的图像解释:
代理:提供了修改底层实现的方式
//代理一个目标对象
//target:目标对象
//handler:是一个普通对象,其中可以重写底层实现
//返回一个代理对象
new Proxy(target, handler)
使用
const obj = {
a: 1,
b: 2
}
const proxy = new Proxy(obj, {
set(target, propertyKey, value) {//target就是obj
//target[propertyKey] = value; //代理帮对象的属性赋值
Reflect.set(target, propertyKey, value); //一样的效果,但是使用Reflect并不会影响到底层代码
},
get(target, propertyKey) { //可以自己修改底层js功能,但是是通过重写Reflect反射的方法,实现重写底层
if (Reflect.has(target, propertyKey)) { //查看是否有该属性
return Reflect.get(target, propertyKey);
} else { //没有则返回-1
return -1;
}
},
has(target, propertyKey) {
return false; //修改底层has(),查找属性时,直接告诉它没有该元素
}
})
obj.a = 10; // 此处就会触发set(obj,{set(obj,a,10)})
console.log(proxy.a); //10 注意:使用代理后,调用属性也是通过代理调用
console.log(proxy.c); //-1
console.log('a' in proxy); //false
代理和反射的关系
简单理解就是:代理提供一种机制,可以重写反射的底层方法,实现重写底层原理
观察者模式
有一个对象,是观察者,它用于观察另外一个对象的属性值变化,当属性值变化后会收到一个通知,可能会做一些事。
其实就是相当于把代理换成观察者,意思差不多
用以前的方式实现观察者模式
function observer(target) {
const div = document.getElementsByTagName('div')[0];
const ob = {};
const props = Object.keys(target); //获取target所有属性名
for (const prop of props) {
Object.defineProperty(ob, prop, { //让ob的每一个属性都配置get() set()
get() {
return target[prop];
},
set(val) {
target[prop] = val;
render();
},
enumerable: true //注意这里默认是false(不可枚举),需要设置为可枚举,让render()遍历
})
}
function render() {
let html = '';
for (const prop of Object.keys(ob)) { //遍历ob属性名
html += `<p>
<span>${[prop]}:</span>${ob[prop]}<span></span>
<p>`
}
div.innerHTML = html;
}
return ob;
}
const target = {
a: 1,
b: 2
}
const obj = observer(target);
obj.a = 10; //设置a的值,target也会改变,并且页面渲染也会改变
缺点:搞出了两个对象,并且如果target添加新的属性,观察者obj并没有改变
通过代理Proxy实现
function observer(target) {
const div = document.getElementsByTagName('div')[0];
const proxy = new Proxy(target, { //相当于在target外面套一层代理proxy对象,当target属性需要操作时,会通过proxy对target进行处理
get(target, propertyKey) { //先走proxy代理进行处理(此处就能实现一些其他的操作了),然后proxy再让Reflect对target进行设置
return Reflect.get(target, propertyKey);
//通过Reflect反射,可以捕获到底层实现,所以使用Reflect之后,即使代理proxy的属性新增、删除、修改、查找等操作,都能监测到
},
set(target, propertyKey, value) {
Reflect.set(target, propertyKey, value);
render();
}
})
render();
function render() {
let html = '';
for (const prop of Object.keys(target)) { //遍历ob属性名
html += `<p>
<span>${[prop]}:</span>${target[prop]}<span></span>
<p>`
}
div.innerHTML = html;
}
return proxy;
}
const target = {
a: 1,
b: 2
}
const obj = observer(target);
obj.a = 10; //设置a的值,target也会改变,并且页面渲染也会改变
obj.c = 30; //新增属性也能被捕获到,页面渲染也会改变
使用proxy实现偷懒构造函数
可以方便实例对象,减少代码量(指的是那些this.属性名 = 属性值,这些操作)
但是不是一定要用这个东西,有时候可能构造函数还有其他操作
//偷懒构造函数
class User {};
function ConstructorProxy(Class, ...propName) { //propName:形参列表
return new Proxy(Class, { //返回一个User代理对象
//一旦代理对象被new 实例,底层就会调用construct
construct(target, argumentsList) { //重写construct
//target是Class,argumentsList是传进来的实参列表
//通过Reflect对象调用底层construct方法
const obj = Reflect.construct(target, argumentsList) //用Class的构造函数创建一个对象
propName.forEach((item, index) => {
obj[item] = argumentsList[index]; //让obj的属性和传进来的值对应
})
return obj;
}
})
}
const user = ConstructorProxy(User, 'firstName', 'lastName'); //user是User的代理对象
const u = new user('小', '名');
console.log(u); //{firstName: "小", lastName: "名"}
使用proxy实现函数实参类型验证
function sum(a, b) { //因为有时候可能传进来的a和b不一定是number类型,所以可以设置代理proxy进行处理
return a + b;
}
function validatorFunction(func, ...type) { //type是设置参数的类型
return new Proxy(func, {//因为函数也是一个对象,所以返回的proxy对象也可以当作函数使用
apply(target, thisAgument, argumentList) { //thisArgument是this指向
//判断实参是否是number
type.forEach((item, index) => {
if (typeof argumentList[index] !== item) {
throw new TypeError(`第${index}个实参的类型不是${item}!`);
}
})
//通过Reflect实现底层调用
Reflect.apply(target, thisAgument, argumentList);
}
})
}
const sumV = validatorFunction(sum, 'number', 'number');
console.log(sumV('1', 2)); //TypeError: 第0个实参的类型不是number!
新增的数组API
静态方法
-
Array.of(…args): 使用指定的数组项创建一个新数组
与直接 new Array()的区别
Array.of() const arr = Array.of(1,2,3);//[1,2,3] 虽然通过new Array()也能创建数组,如: const arr = new Array(1,2,3);//[1,2,3] 但是,如果传的参数只有一位数时,则Array会识别为数组长度,如: const arr = new Array(1);//[empty] length = 1
-
Array.from(arg): 通过给定的类数组 或 可迭代对象 返回一个新的数组。(把类数组 或 可迭代对象 转换成真正的数组)
实例方法
find(callback): 用于查找满足条件的第一个元素(其实跟filter使用基本一样)
**和filter的区别:**filter会全部找出来,而 find 只会找满足要求的第一个
const arr = [
{
id:1,
age:19
},
{
id:2,
age:20
}
];
arr.find(item=>{//item数组中的每一项
if(item.id == 1){
return true;
}else{
return false;
}
})
findIndex(callback):用于查找满足条件的第一个元素的下标
fill(data):用指定的数据填充满数组所有的内容
const arr = new Array(100);
arr.fill('abc');//将数组的每一项都填充为'abc'
copyWithin(target, start?, end?): 在数组内部完成复制
const arr1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
arr1.copyWithin(4); //从下标为4的位置开始复制数组下标0到最后的数据
console.log(arr1); //[1, 2, 3, 4, 1, 2, 3, 4, 5, 6]
const arr2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
arr2.copyWithin(4, 2); //从下标为4的位置开始复制数组下标为4到最后的数据
console.log(arr2); //[1, 2, 3, 4, 3, 4, 5, 6, 7, 8]
const arr3 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
arr3.copyWithin(4, 2, 4); //从下标为4的位置开始复制数组下标为2到4之前的数据
console.log(arr3); //[1, 2, 3, 4, 3, 4, 7, 8, 9, 0]
includes(data):判断数组中是否包含某个值,使用Object.is匹配(+0 != -0)
类型化数组
数字存储的前置知识
-
计算机必须使用固定的位数来存储数字,无论存储的数字是大是小,在内存中占用的空间是固定的。
-
n位的无符号整数能表示的数字是2^n个,取值范围是:0 ~ 2^n - 1
-
n位的有符号整数能表示的数字是2n个,取值范围是:-2(n-1) ~ 2^(n-1) - 1
-
浮点数表示法可以用于表示整数和小数,目前分为两种标准:
- 32位浮点数:又称为单精度浮点数,它用1位表示符号,8位表示阶码,23位表示尾数
- 64位浮点数:又称为双精度浮点数,它用1位表示符号,11位表示阶码,52位表示尾数
-
JS中的所有数字,均使用双精度浮点数保存
简单理解就是:在JS中,每个数字所占用的内存就是64位
1 byte(字节) = 8 bit(位) 1 mb = 1024 byte 1 kb = 1024 mb 1 gb = 1024 kb 如果要存储100个0 那么内存 = 64 * 100 = 6400位 也就是 800mb 将近1kb内存类型化数组
类型化数组:用于优化多个数字的存储
具体分为:
- Int8Array: 8位有符号整数(-128 ~ 127)
- Uint8Array: 8位无符号整数(0 ~ 255)
- Int16Array: …
- Uint16Array: …
- Int32Array: …
- Uint32Array: …
- Float32Array:
- Float64Array
- 如何创建数组
new 数组构造函数(长度) 数组构造函数.of(元素...) 数组构造函数.from(可迭代对象) new 数组构造函数(其他类型化数组)
const arr = Int32Array.of(35111,7,3,11); const arr1 = new Int8Array(arr); console.log(arr === arr1);//false //因为当35111在32位数组中是在可存储范围内的,但是在8位数组的可存储范围外,所以导致出现误差,以至于不相等
得到长度
数组.length //得到元素数量 数组.byteLength //得到占用的字节数
- 其他的用法跟普通数组一致,但是:
- 不能增加和删除数据,类型化数组的长度固定
- 一些返回数组的方法,返回的数组是同类型化的新数组
ArrayBuffer
ArrayBuffer:一个对象,用于存储一块固定内存大小的数据。
new ArrayBuffer(字节数)
可以通过属性byteLength
得到字节数,可以通过方法slice
得到新的ArrayBuffer
读写ArrayBuffer
- 使用DataView
通常会在需要混用多种存储格式时使用DataView
- 使用类型化数组
实际上,每一个类型化数组都对应一个ArrayBuffer,如果没有手动指定ArrayBuffer,类型化数组创建时,会新建一个ArrayBuffer
const bf = new ArrayBuffer(10); //10个字节的内存
const arr1 = new Int8Array(bf);
const arr2 = new Int16Array(bf);
arr2[0] = 2344;//操作了两个字节(16位),如果数值在8位的范围内的话,操作一位字节
console.log(arr1 === arr2);//false
console.log(arr1.buffer === arr2.buffer);//true 使用的是同一个内存
throw new TypeError(`第${index}个实参的类型不是${item}!`);
}
})
//通过Reflect实现底层调用
Reflect.apply(target, thisAgument, argumentList);
}
})
}
const sumV = validatorFunction(sum, 'number', 'number');
console.log(sumV('1', 2)); //TypeError: 第0个实参的类型不是number!
## 新增的数组API
## 静态方法
- **Array.of(...args)**: 使用指定的数组项创建一个新数组
**与直接 new Array()的区别**
```js
Array.of()
const arr = Array.of(1,2,3);//[1,2,3]
虽然通过new Array()也能创建数组,如: const arr = new Array(1,2,3);//[1,2,3]
但是,如果传的参数只有一位数时,则Array会识别为数组长度,如: const arr = new Array(1);//[empty] length = 1
- Array.from(arg): 通过给定的类数组 或 可迭代对象 返回一个新的数组。(把类数组 或 可迭代对象 转换成真正的数组)
实例方法
find(callback): 用于查找满足条件的第一个元素(其实跟filter使用基本一样)
**和filter的区别:**filter会全部找出来,而 find 只会找满足要求的第一个
const arr = [
{
id:1,
age:19
},
{
id:2,
age:20
}
];
arr.find(item=>{//item数组中的每一项
if(item.id == 1){
return true;
}else{
return false;
}
})
findIndex(callback):用于查找满足条件的第一个元素的下标
fill(data):用指定的数据填充满数组所有的内容
const arr = new Array(100);
arr.fill('abc');//将数组的每一项都填充为'abc'
copyWithin(target, start?, end?): 在数组内部完成复制
const arr1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
arr1.copyWithin(4); //从下标为4的位置开始复制数组下标0到最后的数据
console.log(arr1); //[1, 2, 3, 4, 1, 2, 3, 4, 5, 6]
const arr2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
arr2.copyWithin(4, 2); //从下标为4的位置开始复制数组下标为4到最后的数据
console.log(arr2); //[1, 2, 3, 4, 3, 4, 5, 6, 7, 8]
const arr3 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
arr3.copyWithin(4, 2, 4); //从下标为4的位置开始复制数组下标为2到4之前的数据
console.log(arr3); //[1, 2, 3, 4, 3, 4, 7, 8, 9, 0]
includes(data):判断数组中是否包含某个值,使用Object.is匹配(+0 != -0)
类型化数组
数字存储的前置知识
-
计算机必须使用固定的位数来存储数字,无论存储的数字是大是小,在内存中占用的空间是固定的。
-
n位的无符号整数能表示的数字是2^n个,取值范围是:0 ~ 2^n - 1
-
n位的有符号整数能表示的数字是2n个,取值范围是:-2(n-1) ~ 2^(n-1) - 1
-
浮点数表示法可以用于表示整数和小数,目前分为两种标准:
- 32位浮点数:又称为单精度浮点数,它用1位表示符号,8位表示阶码,23位表示尾数
- 64位浮点数:又称为双精度浮点数,它用1位表示符号,11位表示阶码,52位表示尾数
-
JS中的所有数字,均使用双精度浮点数保存
简单理解就是:在JS中,每个数字所占用的内存就是64位
1 byte(字节) = 8 bit(位) 1 mb = 1024 byte 1 kb = 1024 mb 1 gb = 1024 kb 如果要存储100个0 那么内存 = 64 * 100 = 6400位 也就是 800mb 将近1kb内存类型化数组
类型化数组:用于优化多个数字的存储
具体分为:
- Int8Array: 8位有符号整数(-128 ~ 127)
- Uint8Array: 8位无符号整数(0 ~ 255)
- Int16Array: …
- Uint16Array: …
- Int32Array: …
- Uint32Array: …
- Float32Array:
- Float64Array
- 如何创建数组
new 数组构造函数(长度) 数组构造函数.of(元素...) 数组构造函数.from(可迭代对象) new 数组构造函数(其他类型化数组)
const arr = Int32Array.of(35111,7,3,11); const arr1 = new Int8Array(arr); console.log(arr === arr1);//false //因为当35111在32位数组中是在可存储范围内的,但是在8位数组的可存储范围外,所以导致出现误差,以至于不相等
得到长度
数组.length //得到元素数量 数组.byteLength //得到占用的字节数
- 其他的用法跟普通数组一致,但是:
- 不能增加和删除数据,类型化数组的长度固定
- 一些返回数组的方法,返回的数组是同类型化的新数组
ArrayBuffer
ArrayBuffer:一个对象,用于存储一块固定内存大小的数据。
new ArrayBuffer(字节数)
可以通过属性byteLength
得到字节数,可以通过方法slice
得到新的ArrayBuffer
读写ArrayBuffer
- 使用DataView
通常会在需要混用多种存储格式时使用DataView
- 使用类型化数组
实际上,每一个类型化数组都对应一个ArrayBuffer,如果没有手动指定ArrayBuffer,类型化数组创建时,会新建一个ArrayBuffer
const bf = new ArrayBuffer(10); //10个字节的内存
const arr1 = new Int8Array(bf);
const arr2 = new Int16Array(bf);
arr2[0] = 2344;//操作了两个字节(16位),如果数值在8位的范围内的话,操作一位字节
console.log(arr1 === arr2);//false
console.log(arr1.buffer === arr2.buffer);//true 使用的是同一个内存