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 的监听器。