es6详解

17 篇文章 0 订阅
2 篇文章 0 订阅

1.let 声明块级作用域变量

{}标记块级作用域

    

    我们来思考一下为什么要用let,肯定就是因为var在某一种情况下会导致一些错误无法得到我们预期的结果,这个let就是为了解决这个问题而出生的。

 

var的不足之处:

    不足一:这是一个很经典的问题

var arr = [ ];

for(var i=0;  i<10;  i++){

    arr [i] = function(){

         alert(i)

    }

}

arr [8](); //结果:10

上面变量不会得到我们想要的结果是因为var声明的变量i 的值会影响到各个块里面的 i,(i是局部变量,arr[i]中定义的函数都指向了同一个i),也就是说当我们第一次进入循环的时候i为0,但经过十次循环后,i已经变成10了,而此时所有的i都变成了10。因为每个arr[]的值都是alert(i)而到了最后i已经是变成10,所以无论是哪个arr,最后都是会弹出10;(就是这个i是共用的,因为i的作用域对于每个arr来说都是一样的)

但是换成了let来定义i之后,因为是块级作用域,也就是每个循环的作用域都不一样,即每个arr拥有的都是一个独立的i。所以此时可以得到我们想要的结果。

 

不足二:用var声明变量的时候会出现变量提升的现象((function(){}();的写法是立即执行函数的意思待会解释)

var a = 1;

(function(){

   alert(a);

   var a = 2;

})();//

会出现undefined的 原因就在于我们在代码块(函数内)里面还声明并定义了一个变量a,导致变量提升了,实际的代码执行顺序是这样的,仔细看完你就知道什么叫变量提升了。

var a = 1;

(function(){

   var a;

   alert(a);

   a = 2;

})();

  对比一下两段简短的代码:var a = 2; 这句代码被拆分成两部分:声明var a ; 和 定义a = 2;而声明部分被提升(看到了吗?提升两个字出现了)到了代码块的前面,运行的时候自己挪到前面了,这就是“变量提升“,结果就是:先执行声明,接着就执行alert(a);变量a只是声明还没定义,就弹出了undefined了。

 

 

    使用let关键字需要注意的几点:

  • 同一个块级作用域内,不允许重复声明同一个变量

  • 函数内不能使用let重新声明函数的参数

  • 不存在变量提升,必须先声明再使用

 

 

 

2.const 声明常量

在定义常量之后是不能再重新定义一次这个常量,它禁止的是定义常量的这个动作,而不是值,例如

const fruit = [];

fruit.push("apple");

fruit.push("lemon");

fruit.push("orange");

console.log(fruit);

 

常量的特点

  • 不可修改,一经定义,在后续的代码中不允许再修改

  • 只在块级作用域起作用

  • 不存在变量提升,必须先声明后使用

  • 不可重复声明同一个变量

  • 声明后必须要赋值

 

那如果常量是一个变量怎么办呢?

const Person = {"name":"张三"};

Person.name = "李四";

Person.age = 20;

console.log(Person);

再看一段代码

var student1 = {"name":"张三"};

var student2 = student1;

student2.name = "李四";

console.log(student1);

console.log(student2);

从第二段代码可以看到,将student1传给student2,当student2的值发生改变的时候,student1也会随之发生改变,这是因为这个赋值是传址赋值,在赋值过程中,变量实际上存储的是数据的地址(对数据的引用),而不是原始数据或者数据的拷贝,所以当student1赋值给student2的时候,将自己的对应的数据地址传给了student2,当student2修改变量的值的时候,student1也会发生变化,

 

用const来声明一个对象类型的常量,就是传址赋值。而不可修改的是对象在内存中的地址,而不是对象本身(不可变的是你家的地址,而不是你家的门)。

但是如果写成下面这样就会报错

const Person = {"name":"张三"};

    Person.age = 20;

    Person = {};

此时企图给常量赋新值(新地址)

 

 

理解闭包中的let/const的for循环:

 

 

for循环分为三部分

  • 变量声明部分

  • 循环退出的条件

  • 每次循环最后要执行的表达式

 

也就是说,在上面使用var来定义变量的时候,第一部分只会执行一次,后面两个部分会在每次循环的时候都会执行一次。换句话就是说,变量只声明定义一次。

 

而不同的是,使用let/const来声明变量的for循环,处理会创建块级作用域,还会let/const还会将它绑定到每个循环,也就是每次循环都会重新赋值。

 

 

 

!!let和const都不存在变量提升的情况!!

 

 

在预编译的阶段,JS编译器会先解析一遍判断是否有let/const声明的变量,如果在一个花括号中存在使用let/const声明的变量,则ES6规定这些变量在没声明前是无法使用的,随后再是进入执行阶段执行代码

这里当满足if的条件时,进入true的逻辑,这里因为使用了let声明了变量name,在一开始就"劫持了这个作用域",使得任何在let声明之前使用name的操作都会报错

 

 

3.解构赋值

什么是解构赋值

官方文档中的解析:从数组和对象中提取值,对变量进行赋值,这被称为解构。数组解构的原理其实是消耗数组的迭代器,把生成对象的value属性的值赋值给对应的变量

(1).数组的解构赋值(左边不允许有数组名)

在以往的js中,如果需要将一个数组的值分别赋给不同的变量就是像下面这样

var tem = ['apple','orange','tea'];

var des1 = tem[0],des2 = tem[1],des3 = tem[2];

console.log(des1,des2,des3);

在ES6中可以直接分解数组:

 

let [des1,des2,des3] =  ['apple','orange','tea'];

console.log(des1,des2,des3);

 

上面两个的输出结果也是一致的

 

数组的解构赋值需要注意的集中情况

  • 结构赋值是可以嵌套的。数组中即使再嵌套另一个数组,结构赋值也能为我们的变量准确的赋值

let [ a,b,[ c1,c2 ] ] = [ 1,2,[ 3.1,3.2 ] ];

console.log(a);

console.log(c2);

  • 不完全解构

当左边的模式(格式)与右边不完全一样的时候,那么赋值的过程中,只会给模式匹配成功的部分的变量赋值。所以c没有在右边找到匹配的模式,所以它的值为undefined。但这并不影响a,b的值,因为它们能在右边找到对应的模式。

var [a,b,c] = [1,2];

console.log(a);//结果:a的值为1

console.log(b);//结果:b的值为2

console.log(c)//undefined

 

  • 赋值不成功,变量的值为undefined(就像上面例子的c),解构不成功,变量的值就等于undefined。相当于只声明了变量c,但是没赋值。

  • 允许设定默认值

在下面的代码中,左边的数组中c已经被初始化为3,所以即使右边中没有它对应的值,它也是一个有效的值。如果右边有对应的模式,就会覆盖掉这个值。

var [a,b,c=3] = [1,2];

console.log(a);//结果:a的值为1

console.log(b);//结果:b的值为2

console.log(c);//结果:c的值为3

 

 

(2).解构对象

let {dersert,drink,fruit}

        =

        {

            dersert : "蛋糕",

            drink:"茶",

            fruit:"苹果"

        };//前面代表属性,后面代表值

console.log(dersert,drink,fruit);

上面代码跟数组的解构赋值很相似,只不过将数组换成了对象。但是两者有一个不同的地方,我们队上面的代码修改一下,就是将右边对象里面属性的顺序换了一下。

let {dersert,drink,fruit}

        =

        {

            dersert : "蛋糕",

            fruit:"苹果",

            drink:"茶",

        };//前面代表属性,后面代表值

console.log(dersert,drink,fruit);

可以发现,即使右边对象属性位置进行了调换,但这并不影响赋值的结果,变量fruit和drink的值并不会改变。这是因为对象的解构赋值不会受到属性的排列次序影响(但是数组会受到影响),它是跟属性名关联起来的,变量名要和属性名一致,才会成功赋值。

 

如果变量找不到与其姓名匹配的属性,则会赋值不成功

但这不是没有办法补救的,如果你想给一个变量名与属性名不一样的变量解构赋值,可以这样写:

 

var { b:a,} = {"b":2};

console.log(a);//结果:a的值为2

 

但是这样写的话,b是没有被定义的,它只是一个过渡的写法,并不拥有变量空间。

 

 

对象解构的特点

  • 对象解构赋值也可以嵌套

var {a:{b}} = {"a":{"b":1}};

console.log(b);//结果:b的值为1

  • 可以指定默认值

var {a,b=2} = {"a":1};

console.log(a,b);//结果:b的值为默认值2

 

 

(3)字符串的解构赋值(一个字符)

这是因为在解构赋值的过程中,字符串被转换成了一个类似数组的对象。变量a,b,c,d,e,f都分别赋上了对应的值。

var [a,b,c,d,e,f] = "我就是斑马啊";

console.log(a);//我

console.log(b);//就

console.log(c);//是

console.log(d);//斑

console.log(e);//马

console.log(f);//啊

(4)解构赋值的用途

  • 交换变量的值

传统做法最常用的就是引入第三个变量来临时存放,如下:

var x = 1;

var y = 2;

var z = x;//第三个变量临时存放x的值

x = y;  //把y的值赋给x;

y = z;  //把z的值赋值给y;

console.log(x); //结果:x为2

console.log(y); //结果:y为1

 

但有了解构赋值,交换两个值可以向下面这样

var x = 1;

var y = 2;

[x,y] = [y,x];

console.log(x);//2

console.log(y);//1

 

  • 提取函数返回的多个值

函数只能返回一个值,我们可以将多个值封装在一个数组或者对象中,再用解构赋值快速提取其中的值。像下面的例子中,将demo函数的运行结果直接通过解构赋值赋给变量name和age,时间快速的提取对应的值

function demo(){

    return {"name":"张三","age":21}

}

var {name,age} = demo();

console.log(name);//结果:张三

console.log(age);//结果:21

 

 

  • 定义函数参数

function demo({a,b,c}){

    console.log("姓名:"+ a);

    console.log("身高:"+ b);

    console.log("体重:"+ c);

}

demo({a:"张三",b:"1.72m",c:"50kg",d:"8000"});

 

通过这种写法,很方便就能提取json对象中想要的参数,例如上面代码中,函数的参数只有3个,但是json对象中有4个属性。

 

  • 函数参数的默认值

传统的参数默认值的实现方式,先判断该参数是否为undefined,如果是就给它设定默认值,就像下面这样:

function demo(a){

        var name;

        if(a === undefined){//判断参数书否是否传值

       name= "张三"; //没传,赋默认值

     }else{

            name= a;

        }

        console.log(name);

    }

 

 

但是有了解构赋值,就可以变成下面这样

function demo({name="张三"}){

    console.log("姓名:"+name);//结果:姓名:张三

}

demo({});

demo({name:"krys"});

 

 

4.es6中对字符串的拓展

 

ES6对字符串新增了一些函数和操作规范,使得开发者对字符串的操作更加方便,以往需要借助其他javascript代码才能实现的效果,现在利用这些函数即可快速实现。

 

(1)新特性:模板字符串

“模板字符串”是字符串的一个新特性,传统的字符串实现拼接的时候,要将变量插入字符串中,语法是这样的,

let derst = "蛋糕",fruit = "苹果";

let breakfeast = "今天的早餐是"+derst+"和"+fruit;

console.log(breakfeast);

 

这种写法没什么不好,只是当数据一旦多起来就会很繁琐,你会看到N个加号和引号,es6给我们提供了更加方便的写法,像下面这样

 

let derst = "蛋糕",fruit = "苹果";

let breakfeast = `今天的早餐是 ${derst}${fruit}`;

console.log(breakfeast);

 

上面使用了模板字符串,这样就不用反复的使用引号和加号来拼接字符串了,而是采用了反引号标识符(`),插入变量的时候也不需要再使用加号了,而是把变量放入${}即可。

 

使用模板字符串需要注意的地方

  • 可以定义多行字符串,但是字符串里换行的话会被保留到输出中

let str = `write once ,

       run anywhere`;

console.log(str);

代码直接换行就可以,但是代码上的换行会保留在输出中,可以看到上面的输出结果中,字符串是换行了的。

 

  • ${ }中可以放任意的javascript表达式

${}中可以是运算表达式

var a = 1;

var b = 2;

var str = `the result is ${a+b}`;

console.log(str)//the result is 3

 

${}可以是对象的属性

var obj = {"a":1,"b":2};

var str = `the result is ${obj.a+obj.b}`;//对象obj的属性

console.log(str)//结果:the result is 3.

 

${}可以是函数的调用

 

function fn() {

    return 3;

}

var str = `the result is ${ fn() }`;

console.log(str)

//函数fn的调用,结果:the result is 3

 

 

(2)标签模板

这里的模板指的是上面讲的字符串模板,用反引号定义的字符串;而标签,则指的是一个函数,一个专门处理模板字符串的函数。

 

var name = "张三";

var height  = 1.8;

var weight = "55kg";

tagFn`他叫${name},身高${height}米。体重${weight}`;

//标签+模板字符串

 

//定义一个函数,作为标签

function tagFn(arr,v1,v2,v3){

    console.log(arr);//结果:[ "他叫",",身高","米。" ]

    console.log(v1);//结果:张三

    console.log(v2);//结果:1.8

    console.log(v3);//55kg

}

 

在上面的例子中,tagFn函数是我们自定义的函数(也就是标签),它有四个参数,分别是arr,v1,v2,v3.函数的调用跟平时用的不一样,这里的调用直接在函数名(标签名)后面直接加上一个模板字符串tagFn`他叫${name},身高${height}米。体重${weight}`;。

 

这就是模板标签,就像是标签函数+模板字符串,这是一张新的语法规范。

 

接下来看四个参数,根据打印结果可看出,arr是一个数组,它包含的元素就是模块字符串中的不在${}里面的字符,根据${}来进行切割,每一个就是一个元素。

 

v1,v2,v3分别是${}里面的参数。

 

标签模板是ES6给我们带来的一种新语法,它常用来实现过滤用户的非法输入和多语言转换。

 

(3)repeat函数

repeat函数:将目标字符串重复N次,返回一个新的字符串,不影响目标字符串

  重复4次后返回一个新字符串赋值给name2,name1不受影响,所以name1的值不变。

var name1 = "斑马工作室";  //目标字符串

var name2 =  name1.repeat(4);//重复四次

console.log(name1);

console.log(name2);

(4)includes函数

   includes( )函数:判断字符串中是否含有指定的子字符串,返回true表示含有和false表示未含有。第二个参数选填,表示开始搜索的位置。

var name = "斑马工作室";    //目标字符串

console.log(name.includes('马'));

console.log(name.includes('工',1));

console.log(name.includes('梁'));

console.log(name.includes('梁',1));//从第二个字符开始查找

在es5中查找字符串中是否包含某一个字符使用indexOf();如果含有指定的字符串,indexOf函数就会返回字符串首次出现的位置,不含有就会返回-1.

 

(5)startsWith函数

startsWith函数,判断指定的子字符串是否出现在目标字符串的开头目标位置,第二个参数选填,表示开始搜索的位置。

吗如果判断字符串是否以某个子字符串开头,就可以直接使用startsWIth函数即可。同样,第二个参数为1表示从第二个字符开始搜索。(小技巧:判断某个字符是否在某个位置,可以将第二个参数设置为那个位置)

var name = "斑马工作室";  //目标字符串

console.log(name.startsWith("斑"));

console.log(name.startsWith("马"));

console.log(name.startsWith("马",1));

(6)endsWith

与上面相反,这是一个判断子字符串是否出现在目标字符串的尾部位置,第二个参数选填,表示针对前N个字符

var name = "斑马工作室";  //目标字符串

console.log(name.endsWith("室"));

console.log(name.endsWith("马"));

console.log(name.endsWith("马",2));//对于前两个字符斑马来说,马是这个子字符串的尾端

 

(7)codePointAt函数String.fromCodePoint函数

codePointAt函数返回字符的码点。String.fromCodePoint函数参数是一个字符对应的码点,返回的结果是对应的字符。

例如

let str2 = "?";

let code = str2.codePointAt();

console.log(str2.codePointAt());

console.log(String.fromCodePoint(code));

 

 

 

(8)String.raw函数

返回字符串最原始的样貌,即使字符串含有转义符,都会直接输出

  \n被识别为\和n两个字符,失去换行的效果,直接输出,这就是String.raw( )的功能。它常用来作为一个模板字符串的处理函数,也就是直接在后面加一个模板字符串。

console.log(`hello\nworld`);

console.log(String.raw`hello\nwolrd`);

 

 

5.es6中为数值新增的扩展

 

在ES5中,我们存在几个全局函数 isNaN函数,isFinite函数,parseInt函数,parseFloat函数等,对于这些全局函数的使用很简单,就拿isNaN函数来举例。

es5的写法

isNaN(2.5);  //结果:false

    window.isNaN(2.5);//结果:false

以上两种写法均可,isNaN是全局函数,本身就是属于window对象下的一个方法,所以大部分人会使用第一种写法。

 

但是在ES6的标准中,isNaN方法被移植到了Number对象上,也就是原本属于全局对象window下的函数,现在属于Number对象上了,同样被处理的函数还有isFinite函数,parseInt函数,parseFloat函数

被移植后的函数使用方式是这样的:

Number.isNaN(2.5); //结果:false

在使用之前必须指明它是Number对象下的函数,否则会被默认为window下的函数。

 

(1)新特性Number.isNaN函数

Number.isNaN函数:用于判断传入的值是否为非数值,是判断非数值而不是判断数值。

Number.isNaN(2.5); //结果:false

 

那么,移植到Number对象isNaN函数和原本是全局函数的isNaN函数,有不一样的地方吗,还是仅仅简单地移植过来就完事了?

 

答案:有区别。

        传统的isNaN函数会把非数值的参数转化成数值再进行判断,而Number. isNaN只对数值类型有效,非数值类型的参数一律返回false。看文字解释不过瘾,咱们看案例。

 

console.log( isNaN('abc'));  //结果:true,'abc'无法转为一个数值,返回true

console.log(isNaN('123'));   //false,isNaN函数将123转换为数值123,这个数是数值,所以返回false

console.log( Number.isNaN('abc'));//结果:false

//'abc'是字符串,Number.isNaN不做类型转换,直接返回false

Number下面的isNaN都懒得给字符串’abc’做类型转换,直接返回false。而ES5中的isNaN函数会对字符串’abc’进行类型转换后,发现它是一个NaN(非数值),才返回true。

        所以我们在使用这个函数到时候还要小心,当返回false的时候,不一定就是一个数值,有可能是一个非数值类型的参数。

 

(2)Number.isFinite函数  

Number.isFinite函数  :用来检查一个数值是否为有穷数。

  Number.isFinite(1);

    //结果:true,数值1是有穷,即非无穷

 

    Number.isFinite(Infinity);

    //结果:false,Infinity表示无穷大的特殊值

   注意第二行代码的参数:Infinity,Infinity是window对象下的一个常量,表示一个无穷数。所以第二行代码会返回false。此外,isFinite函数跟isNaN函数一样,也只是对数值类型有效,对非数值类型的参数一律返回false。

 

(3)Number.parseInt函数  

Number.parseInt函数  :将字符串转换为整数。与原来的parseInt没有区别

//传统用法:

    parseInt('12.3abc');  

    //结果:返回数值12

 

    //ES6用法:

    Number.parseInt('12.3abc');

    //结果:返回数值12

 

(4)新特性:Number.parseFloat函数  

Number.parseFloat函数  :解析一个字符串,并返回一个浮点数,与原来的一样

//传统用法:

    parseInt('12.3abc');

    //结果:返回数值12

 

    //ES6用法:

    Number.parseInt('12.3abc');

    //结果:返回数值12

 

   以上4个函数都是在window对象下,移植到了Number对象下,你可以能会跟我一样好奇:好端端地为什么好移植到其他地方去,这样做的目的是什么?

        其实这么做的目的是慢慢地减少全局性的函数,把全局函数合理地规划到其他对象下,渐渐实现语言的模块化。

 

 

 

(5)新特性:Number.isInteger函数  

Number.isInteger函数  :用来判断是否为整数

Number.isInteger(3.2);

    //结果:false

 

    Number.isInteger(3);

    //结果:true

 

上面的运行结果也如我们所料,数值3.2不是整数,返回false。不过有一点要注意:在javascript内部对整数和浮点数采用一样的存储方式,因此小数点后如果都是0的浮点数,都会被认为是整数

  Number.isInteger(3.0);

    //结果:true

 

    Number.isInteger(3.00);//true

 

(6) Number.EPSILON常量:定义一个极小的数值。

console.log(Number.EPSILON);//2.220446049250313e-16

  Number.EPSILON的出现是用来判断浮点数的计算误差,如果浮点数计算得到的误差不超过Number.EPSILON的值,就表示可以接受这样的误差。s

 

(7)安全整数

JavaScript能够准确表示的整数范围在-2^53~2^53之间,超过这个范围,无法精确表示这个数值。故称之为不安全

 

为此,ES6定义两个常量来表示这个范围的最大值和最小值:Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER。此外,如果给你一个数值,你不知道它是否超出了这个安全范围,你可以使用ES6给我们新增的一个函数Number.isSafeInteger来进行判断。看例子:

Number.isSafeInteger(Number.MAX_SAFE_INTEGER);

    //结果:true

 

    Number.isSafeInteger(Number.MAX_SAFE_INTEGER+1);

    //结果:false

 

我们用最大安全整数Number.MAX_SAFE_INTEGER来做试验,第一行代码的结果返回的值是true,也就表示Number.MAX_SAFE_INTEGER属于安全范围,第二行代码,我们对Number.MAX_SAFE_INTEGER进行了+1,相加后的数值超过安全范围,isSafeInteger函数就返回了false,表示不在安全范围内。

 

S6给数值带来的扩展,除了对Number对象进行了扩展,还对Math对象进行了扩展。对于Math对象大家应该不会感到陌生,我们平时用的求随机数的方法random就是属于Math对象下的方法。

console.log( Math.random());//0.47949819794581283

 

Math对象下的新特性

(8)新特性:Math.trunc函数

Math.trunc函数:用于去除一个数的小数部分,返回整数部分。

  Math.trunc(3);

    //结果:3

 

    Math.trunc(3.1);

    //结果:3

 

(9)新特性:Math.sign函数

Math.sign函数:用来判断一个数到底是正数、负数、还是零。

Math.sign(3);

    //结果:1

 

    Math.sign(-3);

    //结果:-1

 

    Math.sign(0);

    //结果:0

 

    Math.sign('abc');

    //结果:NaN

返回的结果类型有点多,我们分别来讲解一下,参数如果是正数,结果返回1;如果是负数,结果返回-1;如果是0,结果返回0;如果是一个非数值类型的参数,结果返回:NaN。

 

(10)新特性:Math.cbrt函数

Math.cbrt函数:用于计算一个数的立方根。

Math.cbrt(8);

    //结果:2

 

    Math.cbrt(27);

    //结果:3

 

 

6.es6为数组做的扩展

 

(1)Array.of()函数

Array.of()函数的作用是将一组值转换成数组

console.log(Array.of(1,2,3,4,5));

(2)Array.from( )函数

Array.from( )函数:将类似数组的对象或者可遍历的对象转换为真正的数组

 

有哪些是类似数组的对象呢?最常见到的就是调用getElementsByTagName方法得到的结果,它就是一个类似数组的结果,

 

<a>1</a>

<a>2</a>

<a>3</a>

<a>4</a>

<a>5</a>

 

<script>

    let ele = document.getElementsByTagName('a');

    console.log(ele instanceof Array, //结果:false,不是数组

    ele instanceof Object )//结果:true,是对象)

 

可以看到变量ele并不是一个数组,而是一个对象Array,一个类似数组的对象Object。

接下来我们用Array.from()对其进行转换后,再进行一次处理。

console.log(Array.from(ele) instanceof Array):;

这个时候我们运行的结果是:true,也就是经过Array.from函数处理返回的结果,已经变成了一个真正的数组。

        Array.from函数其中一个用处就是将字符串转换成数组

let str = 'hello';

console.log(Array.from(str));

 

(3)find函数

函数作用:找出数组中符合条件的第一个元素

let arr = [1,2,3,4,5,6];

let result = arr.find(function(value){

    return value > 2;

});

console.log(result);//3

 

find()函数的参数是一个匿名函数,数组的每个元素都会进入匿名函数执行,知道结果为true,find函数就会返回value的值:3,倘若所有的元素都不符合匿名函数的条件,find函数就会返回undefined。

let arr = [1,2,3,4,5,6];

let result = arr.find(function(value){

    return value > 7;

});

console.log(result);//undefined

 

(4)findIndex( )函数

findIndex( )函数:返回符合条件的第一个数组成员的位置。

let arr = [1,2,3,4,5,6];

let result =arr.findIndex(function(value){

    return value > 4;

});

console.log(result);//4

 

上面代码的结果是4,因为第一个大于4的元素是,而5所在的数组下标是4(从0算起),倘若所有的元素都不符合匿名函数的条件,就会返回-1.

 

(5)fill( )函数

fill( )函数:用指定的值,填充到数组

let arr = [1,2,3];

arr.fill(4);

console.log(arr);

 

   经过fill( )函数处理后的数组arr已经变成了[4,4,4];正如函数名fill(填充)一样。所有元素都被填充为数字4了。如果我想只填充部分元素可不可以呢?可以的,fill( )函数提供了一些参数,用于指定填充的起始位置和结束位置。

let arr = [1,2,3];

arr.fill(4,1,3);

console.log(arr);

上面的代码中第2个参数和第3个参数的意思是:从位置1的元素开始填充数字4,截止到位置3之前,所以是位置1和位置2的元素被数字4填充了,得到的结果:[1,4,4]。

 

(6)entries( )函数

函数作用:对数组的键值进行遍历(即对数组下标和数组元素都进行遍历),返回一个遍历器,可以用for..of对其进行遍历。

for(let [i,v] of ['a', 'b'].entries())

    {

        console.log(i,v);

    }

上面的代码中,我们将entries( )函数返回的一个遍历器,用for...of进行遍历,并打印出结果,能得到数组的键值:0和1,以及对应的数组元素:‘a‘和’b‘。

 

(遍历数组的index和value,遍历器返回的是index和value,如果let后面只接一个变量,则返回一个数组)

(7)keys( )函数

函数作用:对数组的索引键进行遍历,返回一个遍历器。

 

for(let index of ['a', 'b'].keys())

    {

        console.log(index);

    }

 

(8)values( )函数

作用:对数组的元素进行遍历,返回一个遍历器。

for(let value of ['a', 'b'].values())

    {

        console.log(value);

    }

 

 

(9)数组推导(会报错,先不掌握)

数组推导:用简洁的写法,直接通过现有的数组生成新数组。

 

 

7.es6为对象做的扩展

 

1.对象的传统表示法:

let person = {

        "name":"张三",

        "say":function(){

            alert("你好吗?");

        }

    }

 

上面的案例很简单,变量person就是一个对象,对象含有name属性和一个say方法。表示法是用键值对的形式来表示,这就是传统的表示法。

 

 

es6中的写法

var name = "Zhangsan";

var age = 12;

var person = {name,age};

    console.log(person);

 

首先定义两个变量name和age,简单地用两个变量名即可。

 

对象的方法表示:(这种函数写法只允许在对象里面写,在全局作用域里写这样的函数会报错)

//传统的表示法

    var person = {

        say:function(){

            alert('这是传统的表示法');

        }

    };

 

    //ES6的表示法

    var person = {

        say(){

            alert('这是ES6的表示法');

        }

    };

 

 

2.属性名可以是表达式

用字面量定义一个对象的时候,可以用表达式作为对象的属性名或者方法名。

var f = "first";

    var n = "Name";

    var s = "say";

    var h = "Hello";

 

    var person = {

        [ f+n ] : "Zhang",

        [ s+h ](){

            return "你好吗?";

        }

    };

    console.log(person.firstName);

    //结果:Zhang

    console.log(person.sayHello());

    //结果:你好吗?

   注意上面person对象的定义,其中属性名和方法名都是用中括号[ ]包裹着,里面都是一个字符串相加的表达式,这就告诉我们,用字面量(大括号{ })定义对象的时候,属性名和方法名可以是一个表达式,表达式的运算结果就是属性名或者方法名。这点改进会使得对象在实际开发中的使用变得更加的灵活方便,赞!

 

(3)Object.is()函数

数的作用:比较两个值是否严格相等,或者说全等。(比较的是两个单独的值,不能比较两个对象,即使属性全都一样的两个对象,结果也是false)

var str = '12';

var num = 12;

var num2 = 12;

console.log( Object.is(str,num));

console.log(Object.is(num2,num));

 

(4)Object.assign()函数

函数作用:将源对象的属性赋值到目标对象上。第一个参数是目标对象,第二个参数是源参数。(将源对象的属性添加到目标对象上)这么讲肯定是有点抽象的,咱们用案例说话,更直观更形象:

//这个充当目标对象

    let target = {"a":1};

 

    //这个充当源对象

    let origin = {"b":2,"c":3};

 

    Object.assign(target,origin);

 

    //打印target对象出来看一下

    console.log(target);

    //结果 {a: 1, b: 2, c: 3}

注意输出的结果,target对象已经不是{ a:1 }了,而是变成了{a: 1, b: 2, c: 3},经过Object.assign( )函数的处理,源对象的属性被添加到了target对象上。这就是Object.assign( )函数的作用。

 

此外,Object.assign( )函数的参数还可以是多个(至少是两个)。我们在上面的案例稍做修改,加一个参数:

 

//这个充当目标对象

    let target = {"a":1};

 

    //这个充当源对象

    let origin1 = {"b":2,"c":3};

 

    //这个充当源对象

    let origin2 = {"d":4,"e":5};

 

    Object.assign(target,origin1,origin2);

 

    //打印target对象出来看一下

    console.log(target);

    //结果 {a: 1, b: 2, c: 3, d: 4, e: 5}

我们从最后打印出来的结果可以看出,对象origin1和对象origin2的属性都被添加赋值到了对象target上。也就是Object.assign( )函数参数中的源对象可以是一个或者一个以上

 

那么,如果赋值过程中,对象的属性出现了相同的名字怎么办?如果这样,后面的属性值就会覆盖前面的属性值。还是上面的案例稍做修改,看代码:

 

  //这个充当目标对象

    let target = {"a":1};

 

    //这个充当源对象

    let origin1 = {"a":2};

 

    //这个充当源对象

    let origin2 = {"a":3};

 

    Object.assign(target,origin1,origin2);

 

    //打印target对象出来看一下

    console.log(target);

    //结果 {a: 3}

 

每个对象属性都含有属性a,它的值从1到最后变成了3,也就是Object.assign()函数处理的过程中,会把最后出现的属性覆盖前面的同名属性。

        巧妙利用Object.assign( )函数的功能,我们可以完成很多效果,比如:给对象添加属性和方法,克隆对象,合并多个对象,为对象的属性指定默认值。

 

 

(5)Object.getPrototypeOf( )函数

  函数作用:获取一个对象的prototype属性。这里的对象我们用一个自定义类实例出来的对象来演示。(这里涉及到了javascript的面向对象,后面拓展)

 

//自定义一个Person类(函数)

    function Person(){

 

    }

    //函数都有一个预属性prototype对象

    Person.prototype = {

        //给prototype对象添加一个say方法

        say(){

            console.log('hello');

        }

    };

 

 

    //实例化Person对象,赋值给变量allen

    let allen = new Person();

    //调用类的say方法

    allen.say();

    //结果:打印出hello

 

    //获取allen对象的prototype属性

   console.log(  Object.getPrototypeOf(allen));

    //结果:打印{say:function(){.....}}

但是为了大家能看懂,我把注释写得比较详细,前面部分都是关于面向对象的实现。把函数Person用new关键字调用,这个时候函数Person就相当于构造函数或者说是一个类,实例化后是一个对象,这个对象会继承Person类的prototype的属性和方法。上述例子中,也就是对象allen继承了一个say方法,可以直接调用。

        如果你想看看prototype中还有哪些方法和属性,那么,你就可以使用Object.getPrototypeOf( )函数来获取,参数就是allen对象,最后的结果也如我们所料,确实打印出了我们刚开始定义好的内容:一个对象,含有一个say方法{say:function(){.....}}。

 

(6)Object.setPrototypeOf()函数

函数作用:设置一个实例化后的对象的prototype属性。

//实例化Person对象,赋值给变量allen

let allen = new Person();

//调用类的say方法

allen.say();

//结果:打印出hello

//使用Object.setPrototypeOf

Object.setPrototypeOf(

    allen,

    {say(){console.log('hi')}

    });

 

//再次调用类的say方法

allen.say();

上面的代码,我们使用Object.setPrototypeOf()函数对对象的prototype属性进行了修改,具体的修改是重写了say方法。在修改前,我们曾经调用过一次say( )方法,得到的结果是打印hello,修改之后我们再一次调用allen.say( );得到的结果是打印出hi,说明我们修改成功了。

 

 

8、函数的扩展

 

(1).参数的默认值

常见的给函数参数指定默认值的写法如下:

function person(n,a){

        var name = n || 'Zhangsan';

        var age  = a ||  25;

    }

 

通过或运算来实现给函数指定默认参数,但是这样写有一个缺陷,就是参数对应的布尔值不能为false(数字0,空字符串等)。

 

这个时候es6解决了这一个缺陷,下面是es6中给函数指定默认参数的写法:

function person(name = 'Zhangsan',age = 25){

       console.log(name,age);

    }

    

    person();//结果:Zhangsan  25

    person('Lisi',18);//结果:Lisi  18

 

把默认值写在参数入口处,而不再需要在函数体内进行检测,函数体内可以专注于对参数的使用,不用担心传参问题。

 

但是当只有部分的参数需要指定默认值的时候,那么需要设定默认值的参数一定要放在最后,设定默认值的参数后面不允许有没有设定默认值的参数。

 

//错误写法

    function person(age = 25,name){

        console.log(name,age);

    }

    

    //正确写法

    function person(name,age = 25){

       console.log(name,age);

    }

 

上面的person函数,两个参数name和age,其中只有age需要指定默认值,name不需要,那么,age的排序就必须放在最后,name放在前面。也就是有默认值的参数后面不能再跟不需默认值的参数了。

 

另外,只有当传入的参数为undefined,才会触发默认值的赋值,否则,即使参数是0,false,null都不吹触发默认值赋值。

function person(age = 12){

        console.log(age);

    }

    

    person();//结果:12

    person(undefined);//结果:12

 

    person(0);//结果:0

    person(null);//结果:null

 

 

最后需要注意的是:函数的参数是默认声明的,声明过的变量,就不能用let或者const关键字再次声明,否则会报错。(用var就可以)

 

(2).rest参数:”...”操作符(展开操作符,剩余操作符)

如果用在函数参数中,就代表是获取函数剩下部分的参数。

//求和函数,得到的结果赋值到result

    function sum(result,...values){

        //打印values看看是什么

     console.log(values);

        //结果:[1,2,3,4]

 

        //进行求和

     values.forEach(function (v) {

            //求和得到的结果存到result

            result += v;

        });

        //打印出求和的结果看看

     console.log(result);

        //结果:10

    }

 

    //存储求和结果的变量res

    var res = 0;

    //调用sum函数

    sum(res,1,2,3,4);

 

上面的写法中,...values就是rest参数,代表在实参中,除了第一个参数以外,剩余的参数都会被...values获取到。

 

在上面代码可以看到console.log(values)打印的结果是一个数组,数组元素是除了第一个参数以外其它几个参数。

rest参数的用法,首先是表示法:...values(三个点+变量名);其次,values是一个数组;我们要学会这两点即可。

  要注意的是,rest参数必须是函数的最后一个参数,后面不能再跟其他参数

 

res参数还有一种用法叫做扩展操作符

这种用法一般结合数组来使用,把数组的元素用逗号分隔开,组成一个序列。像下面这样:

function sum(a,b) {

       return  a+b ;

    }

 

    let arr = [2,3];

    //用扩展运算法将数组[2,3]转换成2,3  

    sum(...arr);

    //结果:5

 

可以看到sum(...arr)得到的结果是5,也就是将arr数组中的两个数组元素传进了。事实上这里的...相当于展开操作符,就是将数组展开为对应的参数数列,也就是sum(...arr)相当于sum(2,3).

只要含有iterator接口的数据结构,都可以使用扩展运算符

 

 

剩余运算符和扩展运算符的区别就是,剩余运算符会收集这些集合,放到右边的数组中,扩展运算符是将右边的数组拆分成元素的集合,它们是相反的

 

 

(3)箭头函数

箭头函数是一种全新的定义函数的方式,=>

箭头前面的代表的是参数,箭头后面的代表的是函数体

let breakfast  = drink => drink;

 

传入多个函数,并且函数体复杂的写法

let sum = (a,b)=>{return a+b;}

注意上面的参数和函数体部分,如果参数超过1个的话,需要用小括号()括起来,函数体语句超过1条的时候,需要用大括号括起来。

 

 

箭头函数的this指向的是定义时的this(这里是在定义obj对象,所以此时this指向的是obj),而不是执行时的this。

   //定义一个对象

    var obj = {

        x:100, //属性x

        show(){

        //延迟500毫秒,输出x的值

            setTimeout(

               //匿名函数

               function(){console.log(this.x);},

               500

           );

        }

    };

    obj.show();//打印结果:undefined

 

照理说obj.show()的打印结果应该是100才对,可是打印出来的结果却是undefined。

 

这里的问题就出在了this上面(此时的this是执行时的this),当代码执行到setTimeout的时候,此时的this已经变作是window对象(因为setTimeOut是window对象的放大),已经不再是obj对象了,而此时window对象下没有定义x的值,所以打印出来的结果就是undefined。

 

但是如果使用箭头函数来编写同样的一段代码,就会发生不一样的情况

var obj = {

    x:100, //属性x

    show(){

        //延迟500毫秒,输出x的值

        setTimeout(

            //匿名函数

            ()=>{console.log(this.x);},

            500

        );

    }

};

obj.show();//打印结果:100

 

同样的一段代码中,唯一不同的就是setTimeOut函数中,原来的匿名函数被箭头函数替代了。(同时注意此时的箭头函数的写法)

 

箭头函数中的this指向的是定义时的this,而不是执行时的this

 

当定义obj的show方法是,我们在箭头函数编写this.x此时的this.x指向的是obj.x,而在show被调用的时候,this一人指向的是被定义时候所指向的对象,也就是obj对象。

 

 

箭头函数有一下几个特点

  • 箭头函数没有arguments(如果没有,尽量使用剩余运算符代替)

  • 箭头函数没有prototype,没有constructor,即不能用作构造函数(不能用new关键字调用)

  • 箭头函数没有自己的this,它的this是词法的,也就是上下文的this,

 

 

 

9.全新的数据类型:Symbol

 

先来回顾一下JavaScript的数据类型,分别是

  • String 字符串类型

  • Number 数字类型

  • bool 布尔类型

  • Object 对象类型

  • Null 空值

  • Undefined 未定义

 

设计原因:解决对象的属性名冲突

如何使用?

let sm1 = Symbol();

console.log(sm1)

console.log(typeof  sm1)

 

从上面代码可以看出,我们用Symbol()函数创建一个symbol类型的变量,我们打印了一下变量sm1,得到的结果是Symbol(),它代表着一个独立无二的值,我们虽然看不到它长什么样子,它还有点类似于字符串。

 

我们定义了若干个变量,控制台输出的都是Symbol,看起来长得一一模一样,但其实是两个不相等的值。

 

还有一种写法:

 

let sm1 = Symbol('sm1');

    let sm2 = Symbol('sm2');

 

    console.log(sm1);

 

    //结果:Symbol(sm1)

 

 

    console.log(sm2);

 

这样的写法,Symbol函数接收一个参数,更容易区分变量。

 

需要注意的是,即使参数一样,描述一样,得到的两个值,得到的两个值也是不相等的值,

 

   let sm1 = Symbol('sm');

    let sm2 = Symbol('sm');

 

    sm1 === sm2 //结果:false

 

即使两个变量的描述都是sm,但是终究对应的值还是不一样的,symbol永远都是独一无二的值,

 

 

结合对象来使用symbol

    let name = Symbol();

    let person = {

        [name]:"张三"

    };

 

 

    console.log(person[name]);

    //结果:张三

 

    console.log(person.name);

    //结果:undefined

 

在上面的代码中,我们定义了一个symbol类型的值name,然后再person的对象里面,它是作为对象person的属性,对应的值是张三。

 

首先我们用中括号的方式获取name属性,可以正确获取到了。我们用点运算符的形式获取name属性的时候,获取失败了。原因是当symbol值作为对象的属性名的时候,不能用点运算符获取对应的值。

 

此外,把一个symbol类型的值作为对象的属性名的时候,一定要用中括号[],不能用点运算符,因为用点运算符的话会让JavaScript把后面的属性名理解为一个字符串类型而不是symbol类型。

 

 

let name = Symbol();

    let person = {};

 

    person.name = "张三";

 

    person[name];   //结果:undefined

    person['name']; //结果:张三

    person.name;    //结果:张三

 

 

 

其中变量name是symbol,但是给person对象设置属性的时候,用的是点运算符person.name,而不是中括号person[name],这导致person对象中的属性name实际上是字符串类型的。

person[ name ]这句代码相当于要求javascript去person对象内找一个symbol类型的属性name,不好意思,没有,找不到。person对象只有一个字符串类型的属性name;所以,如果用person[‘name’]或者peroson.name获取的话,就能找到对应的属性name了。

 

需要注意的是,当symbol类型的值作为属性名的时候,该属性是不会出现在for ..in 和for ...of中,也不会被Object.keys()获取到。

 

//定义一个symbol类型的变量name

    let name = Symbol();

 

    //定义一个含有两种类型属性的对象

    let person = {

        [name]:"张三",  //symbol类型

        "age":12        //string类型

    };

 

    Object.keys(person);//结果:["age"]

 

    for(let key in person){

        console.log(key);

    }

 

    //打印结果:age

 

 

如果我们想要获取symbol类型的值,可以用Object.getOwnPropertySymbols(),它会找到symbol类型的属性并且返回一个数组,数组的成员就是symbol类型的属性值。

/定义两个symbol类型的变量name,age

    let name = Symbol("name");

    let age = Symbol("age");

 

 

    let person = {

        [name]:"张三", //symbol类型

        [age]:12       //symbol类型

    };

 

    Object.getOwnPropertySymbols(person);

    //结果:[Symbol(name), Symbol(age)]

 

这样的话,获取字符串类型的属性和获取symbol类型的属性要分开两种不同的方式来获取,难免有有时候会很不方便,有木有什么办法让我们一次性获取所有类型的属性,不管它是字符串类型还是symbol类型呢?

 

有的,我们可以用Reflect.ownKeys( )方法实现:

//定义一个对象,含有两种类型的属性

 

    let person = {

        [Symbol('name')]:"张三",

        "age": 21

 

 

    };

 

    Reflect.ownKeys(person);

 

    //结果:["age",Symbol(name)]

上面的代码汇总,我们先定义一个对象person,它含有两个属性,一个是symbol类型的,一个是字符串类型的,然后我们将对象传入到Reflect.ownKeys()函数中,函数就会给我们返回一个数组,数组的内容便是对象的属性,包括symbol类型和字符串类型。

 

 

(2)Symbol.for()函数

函数作用:根据参数名,去全局环境中搜索是否有以该参数名的symbol值,如果有就返回它,没有就以该参数名来创建一个新的symbol值。

  let n1 = Symbol.for('name');

    let n2 = Symbol.for('name');

    console.log(n1 === n2);

上面的最后一句代码,我们用全相等来对比两个变量进行对比,得到:true,说明n2就是n1,两者相等。

但是上面的代码中定义两个symbol值的时候都是用的Symbol.for而不是Symbol。这两个函数定义symbol值的区别在于。Symbol.for()创建的symbol值会被登记在全局环境中,供以后Symbol.for来搜索。

但是!Symbol()创建的变量就没有这样的效果

let n1 = Symbol('name');

    let n2 = Symbol.for('name');

    console.log(n1 === n2);

 

    //结果:false

 

第一行我们用Symbol()来创建一个symbol值,按照上面的说法,name将不会被登记在全局环境中;所以第二行我们用Symbol.for()去找的时候,是找不到的。此时Symbol.for()将会自动创建一个新的symbol值,也就是所n1和n2是不同的symbol值。

 

(3)Symbol.keyFor()函数

函数作用:返回一个以被登记在全局环境中的Symbol值的key(也就是返回用Symbol.for()所创建symbol值时的参数。)

 

  let n1 = Symbol.for('name');

    Symbol.KeyFor(n1);

    //结果:name

 

   上面的变量n1是被Symbol.for( )创建,不是被Symbol( )创建的,所以用Symbol.keyFor( )去找,是能找到的,会返回这个symbol值的key,也就是它的描述:name。

 

我们再试一下下面的代码:

    let n1 = Symbol('name');

    Symbol.KeyFor(n1);

    //结果:undefined

 

  这段代码的变量n1是用Symbol( )创建的,最后的结果是undefined;这就证明了两个知识点:1、Symbol( )创建symbol值不会被登记在全局环境中供Symbol.for( )和Symbol.keyFor( )搜索;2、Symbol.keyFor( )函数在全局环境中找不到对应的symbol,就回返回undefined。

 

 

 

 

10.Proxy代理

 

先来看看Proxy的使用

  //定义一个对象person

    var person = {"name":"张三"};

 

    //创建一个代理对象pro,代理person的读写操作

    var pro = new Proxy(person,{

        get:function(target,property){

            return "李四"

        }

    });

 

    pro.name;//李四

 

先定义一个对象person,它拥有一个name属性。然后创建一个pro对象,这个对象就是代理对象,对象person的操作都交给pro来(也就是当你想要使用person的时候,你就去用pro)。

 

这就是代理Proxy的作用,将一个对象交给了Proxy代理,然后通过编写处理函数(上面例子的get)来拦截对目标对象的操作。

 

而编写处理函数除了get还有很多。

 

set方法

 

//定义一个对象,含有RMB和dollar属性值

    var bankAccount = {"RMB":1000,"dollar":0};

    //创建一个Proxy代理实例

    var banker = new Proxy(bankAccount,{

        //编写get处理程序

        get:function(target, property){

            //判断余额是否大于0

            if(target[property] > 0){

                //有余额,就返回余额值

                return target[property];

            }else{

                //没钱了

                return "余额不足";

            }    

        },

        //编写set处理程序

        set:function(target,property,value){

            //存入的数额必须是一个数字类型

            if(!Number.isInteger(value)){

                return "请设置正确的数值";

            }

            //修改属性的值

            target[property] = value;

        }

    });

 

    banker.RMB;

    //结果:1000

    banker.dollar;

    //结果:余额不足

 

    //修改dollar属性的值,值是字符串类型

    banker.dollar = "五百";

    banker.dollar;

    //结果:余额不足

 

    //修改dollar属性的值,值是数字类型

    banker.dollar = 500;

    banker.dollar;

    //结果:500

 

几乎每一句代码都有注释,这段代码对应的故事情节是这样的:老王有的银行账户里面有一些存款,其中人民币1000元,美元0元。

var bankAccount = {"RMB":1000,"dollar":0};

 

 有一天,他来到银行柜台前,找到一个叫banker的工作人员,取款之前看看账户里面还有多少钱,然后工作人员banker开始帮他操作(也就是代理)。

 

    banker.RMB;

    //结果:1000

    banker.dollar;

    //结果:余额不足

        banker告诉他:“您账户里面有人民币1000元,可以取款的,但美元余额不足“。

        接着,老王不打算取款了,打算存500美元。.

在填写存款单据的时候,把500不小心写成了“五百“,banker告诫老王:”这样是写不行的,一定要写阿拉伯数字,这样写银行无法帮您存款的“。结果存款失败,账户里面的美元还是0元。

    banker.dollar = "五百";

    banker.dollar;

    //结果:余额不足

        没关系,马上改过来,把“五百“改成500。

  banker.dollar = 500;

    banker.dollar;

    //结果:500

 

        存款成功,账户里面的美元已有500元。

 

故事的整个经过就是这样,有了Proxy代理(银行工作人员bank),帮助老王完成查看银行存款和取款的操作(代理),避免了一些误操作。

get方法拦截了读取操作,set方法拦截了改写操作。Proxy除了支持以上拦截程序,还支持一系列的拦截函数,我们选择几个常用的讲解!

 

(3)ownKeys()方法

ownKeys拦截操作,拦截过滤Object.ownKeys()对象的属性遍历。(可以使用Object.keys(对象)可以遍历对象的属性名

//定义一个对象person,有三个属性

    let person = {"name":"老王","age":40,"height":1.8};

 

    //创建一个代理对象

    let proxy = new Proxy(person,{

        //ownKeys过滤对对象的属性遍历

      ownKeys:function(target){

            return ["name","age"]

        }

    });

 

    Object.keys(person);

    //结果:["name", "age","height"]

    

    Object.keys(proxy);

    //结果:["name", "age"]

 

(4)has()方法

 

拦截keys in object的操作,结果会返回一个布尔值

 

    var person = {

        "name":"张三",

        "age":20

    };

 

    var proxy = new Proxy(person, {

        has: function(target, prop) {

            if(target[prop] === undefined){

                return false;

            }else{

                return true;

            }

        }

    });

 

    "name" in proxy;//结果:true

    "height" in proxy;//结果:false

 

has()方法用于判断是否含有指定的键值对,有就返回true,否则返回false

 

对象含有name属性,所以返回true,没有height属性,返回false。

 

 

(5)apply()方法

处理对象类型的变量可以被代理,函数也可以被代理。如果被代理的变量是一个函数,那么还会支持一个拦截程序:apply调用

//创建一个函数fn

    let fn = function(){

        alert('我是123');

    };

    //创建一个代理实例,代理函数fn

    let proxy = new Proxy(fn,{

        apply:function(){

            alert('我是隔壁老王');

        }

    });

 

    proxy();//结果:我是隔壁老王

   最后一句代码,proxy本身是一个代理实例对象,因为它代理的是一个函数fn,所以可以直接用函数的形式调用proxy( );当它当作函数调用的时候,就会被apply拦截,执行alert('我是隔壁老王')

 

 

(4)如果创建了代理之后又想取消代理的话。我们可以用Proxy.revocable()函数来实现,它会返回一个对象。对象汇总含有一个proxy属性,它就是proxy的代理实例对象;还有一个revoke属性,它是一个方法,用于取消代理。

 

//定义一个对象

    let person = {"name":"张三"};

    //定义一个代理处理程序

    let handle = {

        get:function(target,prop){

            return "李四";

        }

    };

 

    //使用Proxy.revocable()进行代理

    let object = Proxy.revocable(person,handle);

 

    object.proxy.name;//结果:李四

 

    //调用返回对象object的revoke方法,取消代理

    object.revoke();

 

    object.proxy.name;//报错,代理被取消

  这个案例大家要注意的是Proxy.revocable( )方法返回的结果,它是一个对象,在控制台打印出来后的结果是:Object{ proxy:Object , revoke:function(){....} }。有一个proxy属性,它就是Proxy代理实例,还有一个属性revoke,它是一个方法,专用于取消代理。

        我们使用object.proxy.name来读取name的属性,由于被代理拦截了,只能读取到“李四”,接着我们调用revoke( )方法取消代理,然后再使用object.proxy.name的时候就会报错了,代理已经不存在了。

 

 

 

 

11.for ...of

for ...of是一种用于遍历数据结构的方法。

它可以遍历的对象包括数组,对象,字符串,set和map等具有iterator接口的数据结构

 

传统的集中遍历数组的方式

 

(1)

var arr = [1,2,3,4,5];

    for(let i = 0;i<arr.length;i++){

        //...

    }

 

可以看到这样的遍历数组,代码不够简洁

(2)

var arr = [1,2,3,4,5];

    arr.forEach(function (value,index) {

       //...

    });

 

虽然这样写代码简洁了,但是在循环体内无法中断整个循环

 

(3)

  var arr = [1,2,3,4,5];

    for(let i in arr){

        //...

    }

for...in循环更常用于对象的循环,如果用于数组的循环,那么就要注意了,上述代码中每次循环中得到的i是字符串类型,而不是预料中的数字类型,要想对它进行运算,那得先要进行类型转换,造成不方便。

看下面的例子:

var arr = ["a","b","c","d","e"];

for(let i in arr){

 console.log(i);

 }

 

本来上面这段代码的输出结果应该是a,b,c,d,e。但是它实际的输出结果却是0,1,2,3,4

 

所以用i in arr的方式遍历数组,得到的i是数组下标。再来看一下用i in object的效果

var person = {

    name:"张三",

    age:22

};

 

for (let i in person){

    console.log(i)

}

可以看到结果跟我们想要的是一致的。

 

(4)

var arr = [1,2,3,4,5];

    for(let value of arr){

        console.log(value);

    }

    //打印结果:依次输出:1 2 3 4 5

 

    我们列举一下for...of的优势:

1. 写法比for循环简洁很多;

2. 可以用break来终止整个循环,或者continute来跳出当前循环,继续后面的循环;

3. 结合keys( )获取到循环的索引,并且是数字类型,而不是字符串类型。

 

 

优点1:循环可以终止

var arr = [1,2,3,4,5];

    for(let value of arr){

        if(value == 3){

            //终止整个循环

            break;

        }

        console.log(value);

    }

    //打印结果:1 2

 

优点2:可以跳过当前循环

    var arr = [1,2,3,4,5];

    for(let value of arr){

        if(value == 3){

            //跳过当前循环,继续后面的循环

            continue;

        }

        console.log(value);

    }

    //打印结果:1 2 4  5

 

优点3:得到数字类型的索引

var arr = [1,2,3,4,5];

    for(let index of arr.keys()){

        console.log(index);

    }

//打印结果:依次输出:0 1 2 3 4

 

优点4:遍历字符串

let word = "我是斑马

    for(let w of word){

        console.log(w);

    }

    //打印结果:我  是  斑  马

 

优点5:遍历DOM List

<p>1</p>

    <p>2</p>

    <p>3</p>

    //假设有3个p元素

    let pList = document.getElementsByTagName('p');

 

    for(let p of pList){

        console.log(p);

    }

    // 打印结果:<p>1</p>

    //          <p>2</p>

    //          <p>3</p>

 

虽然for ...of可以遍历数组,字符串等数据类型,但是它不能遍历Object对象。看下一节

 

 

12. Es6 中的Iterator 遍历器

 

对于可迭代的数据解构,ES6在内部部署了一个[Symbol.iterator]属性,它是一个函数,执行后会返回iterator对象(也叫迭代器对象,也叫iterator接口),拥有[Symbol.iterator]属性的对象即被视为可迭代的

 

为什么for...of不能遍历对象,看下面的代码

//定义一个的Object对象

    let obj = {"name":"前端君"};

 

    //咱们来for...of一下

    for(let v of obj){

        console.log(v);

    }

结果报错

obj不具有迭代器!!!

 

原来要想能够被for...of正常遍历的,都需要实现一个遍历器iterator。而数组,字符串,set和map就已经内置好了遍历器,它们的原型中都有一个Symbol.iterator方法。

 

现在来验证一下

Array.prototype[Symbol.iterator];

    //结果:function values(){...}

 

    //字符串

    String.prototype[Symbol.iterator];

    //结果:function [Symbol.iterator](){...}

 

    //Set结构

    Set.prototype[Symbol.iterator];

    //结果:function values(){...}

 

    //Map结构

    Map.prototype[Symbol.iterator];

    //结果:function entries(){...}

 

    //Object对象

    Object.prototype[Symbol.iterator];

 

从上面的结果可以看到,唯独Object对象没有Symbol.iteratot属性。

注意:Symbol.iterator 是Symbol 对象的 iterator 属性,是一个特殊的Symbol值,因此,当它作为prototype对象属性名的时候,获取它的时候需要使用[ ]的形式: prototype[Symbol.iterator],不能使用点形式获取:prototype.Symbol.iterator。原因是点形式会把后面的值当作是字符串类型处理,而不是Symbol类

 

也就说,只要一个数据结构拥有一个叫[Symbol.iterator]()方法的数据结构,就可以被for...of遍历,我们称之为:可遍历对象。比如:数组,字符串,Set和Map结构

 

 

(2)Iterator遍历器的原理

  当可遍历对象被for...of遍历的时候,[Symbol.iterator]()就会被调用,返回一个iterator对象。其中还有一个很重要的方法:next( );

let arr = ['a','b','c'];

    //调用数组的Symbol.iterator()方法

    let iter = arr[Symbol.iterator]();

 

 

    iter.next();

    //结果:{value: "a", done: false}

 

    iter.next();

    //结果:{value: "b", done: false}

 

    iter.next();

    //结果:{value: "c", done: false}

 

    iter.next();

    //结果:{value: undefined, done: true}

第1次调用next( )方法:返回数组的第1个元素:“a”,以及done的值为fasle,表示循环没有结束,继续遍历。

 第2次调用next( )方法:返回数组的第2个元素:“b”,以及done的值还是为fasle,表示循环没有结束,继续遍历。

第3次调用next( )方法:返回数组的第3个元素:“c”,以及done的值依然为fasle,表示循环没有结束,继续遍历。

第4次调用next( )方法:返回undefined,以及done的值为true,表示循环结束。

 原来,for...of的原理就是:先调用可遍历对象的[Symbol.iterator]( )方法,得到一个iterator遍历器对象,然后就在遍历器上不断调用next( )方法,直到done的值为true的时候,就表示遍历完成结束了

 

梳理一下:

  • * 可迭代的数据结构会有一个[Symbol.iterator]方法

  • * [Symbol.iterator]执行后返回一个iterator对象

  • * iterator对象有一个next方法

  • * next方法执行后返回一个有value,done属性的对象

 

(3)自定义遍历器

 

既然有了[Symbol.iterator]()方法就算是可遍历对象,那么我给Object对象手动加上一个[Symbol.iterator]()方法,那么它是不是可以被for...of遍历了?

//定义一个的Object对象

    let obj = {

        0:"我是0",

        1:"我是1",

        2:"我是2",

        length:3,

        //添加[Symbol.iterator]方法

        [Symbol.iterator] : function() {

            let _this = this;

            let index = 0;

            return {

                next:() => {

                    let value = _this[index];

                    let done = (index >= _this.length);

                    index++;

                    return {value,done}

                }

            }

        }

    };

 

    //咱们来for...of一下

    for(let v of obj){

        console.log(v);

    }

    //结果:"我是0"

    //      "我是1"

    //      "我是2"

 

创建一个可遍历的对象,并且自定义它的遍历行为。或者说可以通过添加[Symbol.iterator]()方法,把一个不可遍历的Object对象,变成可遍历的对象。

 

   新特性for...of之所以能够遍历各种不同的数据结构,正是因为这个数据结构都实现了Iterator遍历器接口,供for...of遍历。如果没有实现Iterator接口,则该数据结构无法被for...of遍历,比如:普通的Object对象。

 

 

 

 

 

13.Generator函数

 

(1)先来看看Generator函数的模样

//声明一个Hello的Generator函数

    function* Hello(name) {

       yield `hello ${name}`;

        yield `how are you`;

        yield alert("hello,this is endding");

    }

 

由此可以看到generator函数与普通函数的不同之处

  • 普通函数用function来声明,generator函数要那个function*来声明

  • 有一个新的关键字yield

 

下面来看看调用这个函数会发生什么

let iterr = Hello("krys");

 

先看看iterr的值是什么:

 

可以看到iterr是一个Generator函数,拥有next函数。下面来看看调用它的next属性会发生什么?

 

第一次调用next()函数时,返回一个对象,拥有value属性和done属性。

Object { value: "hello ste", done: false }

 

第二次调用next方法,同样也得到了一个对象

Object { value: "how are you", done: false }

第三次调用next方法时,弹出一个hello,this is endding,并返回一个对象

Object { value: undefined, done: false }

第四次调用的时候返回一个对象

Object { value: undefined, done: true }

 

由上面的结果可以发现,每次调用一次next函数,执行一次yeild后面跟着的代码,如果后面跟着的是表达式,则执行这些表达式,返回的对象中value的值为undefined,如果后面跟着的是一些字符串,则value中的值为这些字符串。

 

所以:我们可以把Generator函数被调用后得到的生成器理解成一个遍历器iterator,用于遍历函数内部的状态。

 

所以我们可以得到下面的结论:

 

(2)Generator函数被调用后并不会一直执行到最后,它是先回返回一个生成器对象,然后hold住不动,等到生成器对象的next( )方法被调用后,函数才会继续执行(执行一个yield关键字后面跟着的代码段),直到遇到关键字yield后,又会停止执行,并返回一个Object对象,然后继续等待,直到next( )再一次被调用的时候,才会继续接着往下执行,直到done的值为true。

 

(3)而yield在这里起到了十分重要的作用,就相当于暂停执行并且返回信息。有点像传统函数的return的作用,但不同的是普通函数只能return一次,但是Generator函数可以有很多个yield。而return代表的是终止执行,yield代表的是暂停执行,后续通过调用生成器的next( )方法,可以恢复执行。

 

 

(4)此外,next()方法还可以接受一个参数,这个参数是作为上一个yield的返回值。

var res

function* Hello2() {

    res = yield `hello`;

    yield res;

    yield res;

}

 

let iterator = Hello2();

 

可以看倒hello2方法的函数体中,第一行的yield的返回值赋值给了res

第一次调用next时,返回的结果是hello

第二次调用next的时候,返回的结果是我们传进去的参数,这时再打印一下res的结果可以发现,res的值已经变成了world了。但是res的赋值是在第一个yield的语句中,所以我们已经验证了,next的参数是作为上一次yield语句的返回值。

第三次调用next的时候,虽然我们传进去的值是hhh但是此时res的值仍然是world,这是因为res只有在第一次yield的语句中赋值了,而后并没有改变它的值。

 

(5)调用另一个generator函数:yield*

 

function* gen1() {

    yield "gen1 start";

    yield "gen1 end";

}

 

//声明Generator函数:gen2

function* gen2() {

    yield "gen2 start";

    yield "gen2 end";

}

function* start() {

    yield "start";

    yield* gen1();

    yield* gen2();

    yield "end";

}

 

//调用start函数

var ite = start();

 

 

由上面结果可看到

第一次调用next的时候 返回的是start,也就是第一条yield语句

第二次调用next的时候返回的是gen1start,证明这里已经进入了gen1这个generator函数了。

第三次调用next的时候返回的是gen1 end。仍然是gen1里面的语句。

第四次调用next的时候返回的是gen2 start,证明这里已经进入了gen2这个generator函数了。

第五次调用next的时候返回的是gen2 end。仍然是gen3里面的语句。

第六次调用next的时候返回的是end,此时返回了start函数里面继续执行了。

第七次调用的时候证明已经执行结束了。

 

所以,要想在一个generator函数里面调用另一个generator函数,要用yield*来声明, 如果一个Generator函数A执行过程中,进入(调用)了另一个Generator函数B,那么会一直等到Generator函数B全部执行完毕后,才会返回Generator函数A继续执行

 

 

 

14.Set和WeakSet

1)集合Set

就像数学里面的集合一样,就是一堆值的集合,而且集合里面不会有重复的值。Set结构的成员值是灭有重复的,每个值都是唯一的。如果往集合里面添加已经有的值,会被忽略的。

  var s = new Set();

    console.log(s);// Set []

 

我们看一下实例化的s有哪些属性

 

下面一一来讲解上面的属性

 

(1)往集合里添加元素的两种方法

 

在Set构造函数里面添加(参数必须为数组)

var s = new Set([1,2,3]);

console.log(s);//Set(3) [ 1, 2, 3 ]

 

使用Set的方法add

var s = new Set();

s.add(1);

s.add(2);

s.add("a");

console.log(s);//Set(3) [ 1, 2, "a" ]

 

 

(2)size属性:获取成员的个数

s.size;//3

 

(3)(1)中添加值的时候使用了add函数,那么与之对应的,删除集合里的一个元素则使用delete函数。

当删除成功的时候返回true,删除失败的时候返回false

 

s.delete(2);

 

可以看到当s删除了元素2之后,返回了一个true。当我们再查看s的值时,此时s中已经没有了2了。

 

(4)clear方法:清空集合中所有的值。

 

s.clear();

 

可以看到,当使用clear清空了集合之后,集合变成了一个空集。

 

(5)has()方法:判断set结构中是否含有指定的值。如果有就返回true,如果没有则返回false。

s.has(1);

s.has(3)

可以看到,s原来的值有1,2,a。因为s中含有1,所以s.has(1)返回true,但是s中没有3,所以s.has(3)返回false。

 

 

(6)entries方法

跟数组的entries方法一样,返回一个键值对的遍历器(也就是返回两个值)。值得注意的是,集合里面的键和值是一样的

let iter = s.entries();

//undefined

iter.next();

//Object { value: (2) […], done: false }

iter.next();

//Object { value: (2) […], done: false }

iter.next();

//Object { value: (2) […], done: false }

 

每次调用一次next返回一个对象,我们看看这个对象,里面有一个数组value,可以看到这个数组中含有两个元素,而且相同。所以set里面的键值是一样的。

 

(7)keys函数

返回键名的遍历器、

for (let i of s.keys()){console.log(i);}

//1 2 a

(8)value函数

返回键值的遍历器

for (let i of s.values()){console.log(i);}

//1 2 a 

 

(9)forEach函数

遍历集合里的每一一个成员

s.forEach(function(value,key){console.log(value,key);});

 

 

(10)set的用途

  • 数组去重。当我们想要去掉一个数组中重复的值,可以想下面这样

let arr = [1,2,2,3,4,5,6,7,7,8,6,4,5,3];

let newarr = new Set(arr);//Set(8) [ 1, 2, 3, 4, 5, 6, 7, 8 ]

arr = Array.from(newarr);//Array(8) [ 1, 2, 3, 4, 5, 6, 7, 8 ]

 

 

 

2).weakset

WeakSet结构与Set类似,也是不重复的值的集合。

 

WeakSet和Set的区别:

WeakSet的成员只能是对象(参数中是一个数组,数组里面放对象),而不能是其他类型的值

 

WeakSet中的对象都是弱引用,即垃圾回收机制不考虑WeakSet对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于WeakSet之中。这个特点意味着,无法引用WeakSet的成员,因此WeakSet是不可遍历的。

 

 

 

 

 

 

 

15.Map和WeakMap

1.Map

先看一下map的使用

let m = new Map();

这样就定义了一个Map类型的值m,接下来看一下m的结构

 

可以看到m拥有这么多的方法和属性。

 

所以Map到底是什么?

Map结构有点跟Object对象相似,都是由键值对来组成的一种数据结构。但跟对象不同的是,map的key键名类型不再局限于字符串类型了,它可以是各种类型的值。所以它比object更灵活。

 

(1)map的基本用法

let m = new Map([

    ["name","krys"],

    ["age","22"]

]);

console.log(m);

//Map { name → "krys", age → "22" }

注意参数是一个数组,数组元素也是数组,第一个元素是键名,第二个元素是值,

 

(2)set方法

set方法:给实例设置一堆键值对,返回map实例。

  let m = new Map();

    //set方法添加

 

    //添加一个string类型的键名

    m.set("name","krys");  

  

    //添加一个数字类型的键名

    m.set(1,2);

 

    console.log(m);

    //打印结果:Map {"name" => "krys", 1 => 2}

 

set的使用很简单,只需要给方法传入key和value作为键名和键值即可。(第一个参数是键名,第二个参数是值)。而且我们可以注意到上面代码中,我们添加了一个数字类型的键值,并且成功了。证明,Map结构确实可以存储非字符串类型的键名,当然你还可以设置更多其它类型的键名

 

//数组类型的键名

    m.set([1],2);

 

    //对象类型的键名

    m.set({"name":"Zhangsan"},2);

 

    //布尔类型的键名

    m.set(true,2);

 

    //Symbol类型的键名

    m.set(Symbol('name'),2);

 

    //null为键名

    m.set(null,2);

 

    //undefined为键名

    m.set(undefined,2);

 

使用set方法的时候有一点需要注意,如果你设置一个已经存在的键名,那么后面的键值会覆盖前面的键值。

 

(3)get方法

参数是一个键名,返回对应的值

m.get(null);//2

   get方法使用也很简单,只需要指定键名即可。获取存在对应的键值,如果键值对存在,就会返回键值;否则,返回undefined

 

(4)delete方法

delete( )方法作用:删除指定的键值对,删除成功返回:true,否则返回:false。

 

m.delete([1]);

//false

m.delete(true);

//true

 

(5)clear方法

跟Set结构一样,Map结构也提供了clear方法,让你一次删除所有的键值对

m.clear();

 

(6)has方法

判断map实例内是否含有指定的键值对,有就返回true,否则返回false。

m.has("name");

//true

m.has("age");

//false

   Map实例中含有键名:name,就返回了true,键名age不存在,就返回fals

 

 

(7)entries方法

返回实例的键值对遍历器。

for(let [i,v] of m.entries()){console.log(i+"--"+v);};

//name--krys 

//age--22

(8)keys方法

返回实例中所有键名的遍历器

for(let i of m.keys()){console.log(i);};

(9)values

返回实例中所有键值的遍历器

for(let i of m.values()){console.log(i);};

 

(10)forEach

遍历键值对

m.forEach(function(value,key){

        console.log(key+':'+value);

    });

 

 

(11)size

返回键值对的个数

m.size;

//2

 

2)WeakMap

  WeakMap结构的使用方式和Map结构一样:

let wm = new WeakMap();

 

  两者都是使用new来创建实例。如果添加键值对的话,我们同样是使用set方法,不过键名的类型必须要求是引用类型的值,我们来看看:

let wm = new WeakMap();

 

    //数组类型的键名

    wm.set([1],2);

 

    //对象类型的键名

    wm.set({'name':'Zhangsan'},2);

 

    //函数类型的键名

    function fn(){};

    wm.set(fn,2);

 

    console.log(wm);

 

 

 

(3)WeakMap和Map的区别

 

    如果是普通的值类型则不允许。比如:字符串,数字,null,undefined,布尔类型。而Map结构是允许的,这就是两者的不同之处,谨记。

        跟Map一样,WeakMap也拥有get、has、delete方法,用法和用途都一样。不同地方在于,WeakMap不支持clear方法,不支持遍历,也就没有了keys、values、entries、forEach这4个方法,也没有属性size。

        理由跟WeakSet结构一样:键名中的引用类型是弱引用,你永远不知道这个引用对象什么时候会被垃圾回收机制回收了,如果这个引用类型的值被垃圾机制回收了,WeakMap实例中的对应键值对也会消失。

 

 

 

16.Promise对象

我们先来看一下Promise对象的处理流程

 

let pro = new Promise(function(resolve,reject){

 

    if(true){

        //调用操作成功方法

        resolve('操作成功');

    }else{

        //调用操作异常方法

        reject('操作异常');

    }

});

 

//用then处理操作成功,catch处理操作异常

pro.then(requestA)

    .then(requestB)

    .then(requestC)

    .catch(requestError);

 

function requestA(){

    console.log('请求A成功');

    return '请求B,下一个就是你了';

}

function requestB(res){

    console.log('上一步的结果:'+res);

    console.log('请求B成功');

    return '请求C,下一个就是你了';

}

function requestC(res){

    console.log('上一步的结果:'+res);

    console.log('请求C成功');

}

function requestError(){

    console.log('请求失败');

}

 

下面是处理流程图:

 

 

(2)实战例子

 

let pro = new Promise(function (resolve, reject) {

    if (true){

        //调用操作成功方法

        resolve("操作成功");

        console.log(resolve);

    }else {

        //调用操作异常方法

        reject("操作异常");

        console.log(reject)

    }

});

 

//用then来处理操作成功,catch操作异常

 

pro.then(requestA)

    .then(requestB)

    .then(requestC)

    .catch(requestError);

function requestA() {

    console.log("请求A成功");

    var resa = "";

    $.ajax(

        {

            type:"GET",

            url:"dataA.json",

            dataType:"json",

            success:function (result) {

                $.each(result,function (i,item) {

                    resa += i+"="+item+"-----";

                });

                console.log("请求dataA.json成功,这是A的数据"+resa+"\n");

            }

        }

    );

    return  resa;

}

function requestB(res) {

    console.log("请求B成功,接收到requestA的数据:"+res+"\n");

    var resb = "";

    $.ajax(

        {

            type:"GET",

            url:"dataB.json",

            dataType:"json",

            success:function (result) {

                $.each(result,function (i,item) {

                    resb += i+"-"+item+"-----";

                });

                console.log("请求dataB.json成功,这是B的数据"+resb+"\n");

            }

        }

    );

    return  resb;

 

}

function requestC(res) {

    console.log("请求C成功,接收到requestB的数据:"+res+"\n");

    $.ajax(

        {

            type:"GET",

            url:"dataC.json",

            dataType:"json",

            success:function (result) {

                var resc = "";

                $.each(result,function (i,item) {

                    resc += i+"-"+item+"-----";

                });

                console.log("请求dataC.json成功,这是c的数据"+resc+"\n");

            }

        }

    )

}

function requestError() {

    console.log("请求失败");

}

 

我们从执行结果可以看到,使用了promise对象,就处理了ajax请求中会出现等待的情况(就是要等一个ajax完成了再去处理别的数据)。

第一次调用requestA后,只要进入了函数体,就会立即去调用requestB,等到调用成功requestB后又立马去调用requestC,把这三个函数都加到进程中,然后根据顺序,异步处理这三个函数。

 

(3)promise的三种状态

  • pending:刚刚创建一个Promise实例的时候,表示初始状态

  • fulfilled:resolve方法调用的时候,表示操作成功

  • rejected:rejected方法调用的时候,表示操作失败

 

这三种状态中只存在两种状态转换:pending->fulfilled,pending->rejected。不存在任何其他的状态转换。

 

(4)then方法

用来绑定处理操作后的处理程序(也就是当promise对象创建成功后的处理程序)

(5)catch方法:用来处理操作异常后的业务。

 

  pro.then(function (res) {

        //操作成功的处理程序

    }).catch(function (error) {

        //操作失败的处理程序

    });

    

 

  之所以能够使用链式调用,是因为then方法和catch方法调用后,都会返回promise对象。

 

//用new关键字创建一个Promise实例

    let pro = new Promise(function(resolve,reject){

        //假设condition的值为true

        let condition = true;

 

        if(condition){

            //调用操作成功方法

            resolve('操作成功');

            //状态:pending->fulfilled

        }else{

            //调用操作异常方法

            reject('操作异常');

            //状态:pending->rejected

        }

    });

 

    //用then处理操作成功,catch处理操作异常

    pro.then(function (res) {

 

        //操作成功的处理程序

        console.log(res)

 

    }).catch(function (error) {

 

        //操作失败的处理程序

        console.log(error)

 

    });

    //控制台输出:操作成功

 

 

(6)Promise.all()方法,

接收一个数组作为参数,数组的元素是Promise实例对象,当参数中的实例对象的状态都未fulfiled时,Promise.all()才会有所返回。

let pro1 = new Promise(function(resolve){

        setTimeout(function () {

            resolve('实例1操作成功');

        },5000);

    });

    

    //创建实例pro2

    let pro2 = new Promise(function(resolve){

        setTimeout(function () {

            resolve('实例2操作成功');

        },1000);

    });

 

    

    Promise.all([pro1,pro2]).then(function(result){

        console.log(result);

    });

 

上面的结果是5秒后才打印出来的。

在这个案例中,我们创建了两个Promise对象;pro1,pro2.我们注意两个定时函数的第二个参数,分别是5秒和1秒,当我们调用promise.all的时候,虽然1秒后pro已经进入了fulfilled状态,但是此时promise.all还不会有所行动,因为pro1还没有进入成功fulfilled状态,等到5秒后pro2进入fulfilled状态后,控制台就打印出上面的结果了。

 

这个方法有什么用呢?一般这样的场景:我们执行某个操作,这个操作需要得到需要多个接口请求回来的数据来支持,但是这些接口请求之前互不依赖,不需要层层嵌套。这种情况下就适合使用Promise.all( )方法,因为它会得到所有接口都请求成功了,才会进行操作。

 

 

 

(7)promise.race方法

  //初始化实例pro1

    let pro1 = new Promise(function(resolve){

        setTimeout(function () {

            resolve('实例1操作成功');

        },4000);

    });

 

    //初始化实例pro2

    let pro2 = new Promise(function(resolve,reject){

        setTimeout(function () {

            reject('实例2操作失败');

        },2000);

    });

 

    Promise.race([pro2,pro1]).then(function(result){

        console.log(result);

    }).catch(function(error){

        console.log(error);

    });

 

    同样是两个实例,实例pro1不变,不同的是实例pro2,这次我们调用的是失败函数reject。

        由于pro2实例中2000毫秒之后就执行reject方法,早于实例pro1的4000毫秒,所以最后输出的是:实例2操作失败。

 

 

 

 

 

 

 

 

17 类 class

JavaScript的类class本质上也是基于原型prototype的实现方式做了进一步的封装,

1.基本用法(定义一个类)

class Animal(){

 

        construtor(color){

                this.color = color

        }

}

可以看到上面的代码中我们定义了一个类,类体里面含有一个constructor函数,这个函数就是构造函数,这个函数里面的this指向的是实例化的对象。

 

其中,constructor是一个类必要的一个方法,默认返回实例对象;创建类的实例对象的时候,会先调用这个方法来初始化实例对象。就算我们没有写这个函数,执行的时候也会被带上一个空的constructor函数。

 

(2)类的属性和方法

class Animal {

        //构造方法

        constructor(name){

            //属性name

            this.name = name;

        }

 

        //自定义方法getName

        getName(){

            return this.name;

        }

    }

constructor是必须也是唯一的(one and only one),一个类不能含又多个构造函数。在构造函数里面定义一些对象的属性。例如上面的name属性。

 

(3)类的实例对象

class Animal{

    constructor(name){

        this.name = name;

    }

    getName(){

        return "this is thw"+this.name;

    }

}

let dog = new Animal("dog");

dog.name//"dog"

dog.getName();//"this is thwdog"

 

在上面代码中定义了一个animal的类,类里面含有构造函数和getName函数。再实例化了一个animal类的对象dog。我们调用dog的属性name(这个属性在constructor里面定义了),返回了结果dog,然后我们再调用了dog的getName方法,返回了this is thwdog.

 

需要注意的:

  • 必须要用new来实例化对象

  • 一定要先声明类再实例化对象,不然会报错。

 

(4)类的静态方法

静态方法的意思就是不用实例化一个对象就可以调用了。下面看代码:

class Animal {

        //构造方法

        constructor(name){

            //属性name

            this.name = name;

        }

 

        //自定义一个静态方法

        static friends(a1,a2){

            return `${a1.name} and ${a2.name} are friends`;

        }

    }

 

    //创建2个实例

    let dog = new Animal('dog');

    let cat = new Animal('cat');

    

    //调用静态方法friends

    Animal.friends(dog,cat);

    //结果:dog and cat are friends

 

从上面的代码和执行结果可以看到,静态函数必须要用static关键字来标识,而且是通过类名来调用的。

 

(5)类的继承

//父类Animal

    class Animal {//...}

 

    //子类Dog

    class Dog extends Animal {

        //构造方法

        constructor(name,color){

            super(name);

            this.color = color;

        }

    }

 

从上面的代码中可以看到,我们用extends关键字使Dog类继承了Animal类,并且用super()来调用了父类的构造函数。

class Animal{

    constructor(name){

        this.name = name;

    }

    getName(){

        return "this is thw"+this.name;

    }

    static friend(a1,a2){

        return `${a1.name}和${a2.name}是朋友`;

    }

    say(){

        return `This is a animal`;

    }

}

 

class Dog extends Animal{

    constructor(name,color){

     super(name);

     this.color = color;

    }

    getAttribute(){

        return `${super.say()},name:${this.name},color:${this.color}`;

    }

}

let dog = new Animal("dog");

let cat = new Animal('cat');

let d = new Dog("dog","black");

d.getAttribute();//"This is a animal,name:dog,color:black"

 

在上面的代码中,定义了两个类,其中Dog继承Animal类。其中想要在子类中调用父类的方法,必须要用super来进行调用(super相当于父类的this)

 

使用super需要注意的点

  • 子类中必须在constructor中调用super方法

  • 子类必须要先调用super,才可以使用this,不然会报错。

 

 

 

 

18.闭包

(1)什么是闭包

(2)闭包的优点

(3)闭包的缺陷以及导致的问题

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

下面我们将看到的是 JavaScript 中必须提到的功能最强大的抽象概念之一:闭包。但它可能也会带来一些潜在的困惑。那它究竟是做什么的呢?

function makeAdder(a) {

    return function(b) {

        return a + b;

    }}

var x = makeAdder(5);

这里等价于

var x = function (b) {

    return 5+b;

}

var y = makeAdder(20);

这里也等价于

var x = function (b) {

    return 20+b;

}

 

x(6); //11

y(7); // 27

 

 

JavaScript 执行一个函数时,都会创建一个作用域对象(scope object),用来保存在这个函数中创建的局部变量。它和被传入函数的变量一起被初始化。这与那些保存的所有全局变量和函数的全局对象(global object)类似,但仍有一些很重要的区别,第一,每次函数被执行的时候,就会创建一个新的,特定的作用域对象;第二,与全局对象(在浏览器里面是当做 window 对象来访问的)不同的是,你不能从 JavaScript 代码中直接访问作用域对象,也没有可以遍历当前的作用域对象里面属性的方法。

 

 

所以当调用 makeAdder 时,解释器创建了一个作用域对象,它带有一个属性:a,这个属性被当作参数传入 makeAdder 函数。然后 makeAdder 返回一个新创建的函数。通常 JavaScript 的垃圾回收器会在这时回收 makeAdder 创建的作用域对象,但是返回的函数却保留一个指向那个作用域对象的引用。结果是这个作用域对象不会被垃圾回收器回收,直到指向 makeAdder 返回的那个函数对象的引用计数为零。

作用域对象组成了一个名为作用域链(scope chain)的链。它类似于原型(prototype)链一样,被 JavaScript 的对象系统使用。

 

 

一个闭包就是一个函数和被创建的函数中的作用域对象的组合。

闭包允许你保存状态——所以它们通常可以代替对象来使用。

如果一个函数访问了它的外部变量,那么它就是一个闭包。

  注意,外部函数不是必需的。通过访问外部变量,一个闭包可以维持(keep alive)这些变量。在内部函数和外部函数的例子中,外部函数可以创建局部变量,并且最终退出;但是,如果任何一个或多个内部函数在它退出后却没有退出,那么内部函数就维持了外部函数的局部数据。

内存泄露

 

使用闭包的一个坏处是,在 IE 浏览器中它会很容易导致内存泄露。JavaScript 是一种具有垃圾回收机制的语言——对象在被创建的时候分配内存,然后当指向这个对象的引用计数为零时,浏览器会回收内存。宿主环境提供的对象都是按照这种方法被处理的。

浏览器主机需要处理大量的对象来描绘一个正在被展现的 HTML 页面——DOM 对象。浏览器负责管理它们的内存分配和回收。

IE 浏览器有自己的一套垃圾回收机制,这套机制与 JavaScript 提供的垃圾回收机制进行交互时,可能会发生内存泄露。

在 IE 中,每当在一个 JavaScript 对象和一个本地对象之间形成循环引用时,就会发生内存泄露。如下所示:

function leakMemory() {

    var el = document.getElementById('el');

    var o = { 'el': el };

    el.o = o;}

这段代码的循环引用会导致内存泄露:IE 不会释放被 el 和 o 使用的内存,直到浏览器被彻底关闭并重启后。

这个例子往往无法引起人们的重视:一般只会在长时间运行的应用程序中,或者因为巨大的数据量和循环中导致内存泄露发生时,内存泄露才会引起注意。

不过一般也很少发生如此明显的内存泄露现象——通常泄露的数据结构有多层的引用(references),往往掩盖了循环引用的情况。

闭包很容易发生无意识的内存泄露。如下所示:

function addHandler() {

    var el = document.getElementById('el');

    el.onclick = function() {

        el.style.backgroundColor = 'red';

    }}

这段代码创建了一个元素,当它被点击的时候变红,但同时它也会发生内存泄露。为什么?因为对 el 的引用不小心被放在一个匿名内部函数中。这就在 JavaScript 对象(这个内部函数)和本地对象之间(el)创建了一个循环引用。

这个问题有很多种解决方法,最简单的一种是不要使用 el 变量:

function addHandler(){

    document.getElementById('el').onclick = function(){

        this.style.backgroundColor = 'red';

    };}

另外一种避免闭包的好方法是在 window.onunload 事件发生期间破坏循环引用。很多事件库都能完成这项工作。注意这样做将使 Firefox 中的 bfcache 无法工作。所以除非有其他必要的原因,最好不要在 Firefox 中注册一个onunload 的监听器。

 

 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值