前言
当初学习Es5、Es6时你是不是有一些疑问,关于有些用法、为什么这么用以及这样用的好处你现在搞懂了吗?奋力奔跑的同时别忘了那些可以为你加速的“小伙伴”。
ES5
1. 严格模式:
比普通的js运行更严格的机制,js语言存在很多广受诟病的缺陷,为了弥补缺陷,项目必须运行在严格模式下,在要使用严格模式的作用域顶部添加: “use strict”。严格模式下要求:
- 1.禁止给未声明的变量赋值:
旧js中: 在任何位置,给一个从未声明过的变量强行赋值,结果: 不但不报错!都会在全局创建该变量——导致内存泄漏
ES5中: 只要给从未声明过的变量赋值,也会报错: xxx is not defined
所以,今后,严格模式下,要给一个变量赋值,必须先声明! - 2.静默失败升级为错误:
静默失败就是执行不成功,还不报错!——不便于调试。
ES5中所有静默失败都会以报错方式出现。 - 3.匿名函数自调和普通函数调用中的this不再指向window,而是undefined。
旧js中: 一个函数调用时前边没有.,也没有new,则它的this默认指向window
比如:fun() (function(){ ... })() setTimeout(function(){ ... }, ms)
ES5中: 以上三种情况中的this都是undefined
- 4.禁用了arguments.callee
arguments.callee: 在函数内,自动获得正在调用的函数本身。避免递归调用函数时,造成内外函数名紧耦合,递归调用经常使用。但存在问题:递归算法效率极低,不推荐使用。不过所有的递归算法,都可用循环代替!
2. 保护对象:
防止对一个对象的属性结构和属性值进行无意义的篡改,虽然现实中对对象有很多要求,但是,对象本身毫无自保能力。
-
ES5将对象的属性重新进行了划分:
- 命名属性: 凡是用.可以访问到的属性。
数据属性: 实际存储属性值的属性
访问器属性: 不实际存储属性值,仅提供对其它数据属性的保护。 - 内部属性: 无法用.访问到的属性
比如: isArray视频中讲到的class属性
- 命名属性: 凡是用.可以访问到的属性。
-
如何保护命名属性:
- 保护数据属性:
每个数据属性不再是一个简单的变量,而是一个缩微的小对象,每个小对象中,除了保存属性值之外,还包含三个开关!
每个开关,可以控制对这个属性的一种操作:
那么如何看到每个属性的缩微对象?
var eid=Object.getOwnPropertyDescriptor(对象, "属性名") 获取自己的属性的描述信息 返回值: 一个对象:{ value: 属性值, writable:true, 可 修改 enumerable:true, 可 遍历 configurable:true 可 配置 }
如何修改其中的一个开关呢?不能用.直接访问!
Object.defineProperty( 对象, "属性名", { 开关名: true/false, ... : ... } )
强调: 为了防止重新打开已关闭的开关,只能修改前两个开关时,都要带上configurable:false 作为双保险。因为configurable:false不可逆!
结果: 如果之后的操作违反了开关的要求,比如: 试图修改只读属性的值,再比如试图重新打开已关闭的开关,都会报错(代码运行在严格模式下)!
强调: enumerable:false,其实只是半隐藏,只能防住for in遍历。直接用.还是可以访问到的。问题: Object.defineProperty()一次只能修改一个属性的开关,如果要修改多个属性中的开关,则每次都要重复写一遍,太繁琐,如果要修改多个属性中的开关时:
Object.defineProperties( 对象, { 属性名: { 开关: true/false, ... : ... }, 属性名: { ... : ... } } )
问题: 如果一个属性值不是不能改,也不是不能遍历,只不过这个属性的值的范围是业务自定义的。只用开关,无法如此灵活的保护属性值。
- 访问器属性: 不实际存储属性值,而是提供对另一个数据属性的保护,数据属性的开关,无法用自定义规则灵活保护属性值。如果希望用自定义规则保护属性值就要使用访问器属性。
1.先定义一个隐姓埋名且半隐藏的数据属性,来实际存储属性值。
2.定义访问器属性来保护一个数据属性。
原理:
表面上看,访问器属性的用法和普通属性的用法完全一样。只不过触发的内部原理不同。
//只要试图获取属性值时,都自动调用访问器属性的get()
//只要试图修改属性值时,都自动调用访问器属性的set(value),并把等号后的新值,先交给value参数,先去验证。- 保护对象结构: 禁止对对象的属性结构进行篡改。包括三个级别:
- 防扩展: 禁止向已经创建好的对象中添加新属性。
Object.preventExtensions(obj)
: 禁止向obj对象中添加新属性 - 密封: 在兼具防扩展功能的同时,又进一步禁止删除所有属性
Object.seal(obj)
:禁止向obj中添加新属性,禁止删除obj中现有属性
原理: 其实seal自动将所有属性的configurable都改为false!只密封结构,属性值还是可以修改的! 几乎多有对象都需要密封,来禁止修改结构 - 冻结: 在兼具密封的基础上,进一步禁止修改所有属性值
Object.freeze(obj)
:禁止添加新属性,禁止删除所有属性,禁止修改所有属性的值。
原理: 自动将所有属性的writable改为false。只有那些共用的对象,才会禁止修改属性值。
- 防扩展: 禁止向已经创建好的对象中添加新属性。
- 保护数据属性:
3. Object.create()
即使没有妈妈(构造函数),也能生小孩儿(创建子对象)
var child=Object.create(father, {
//defineProperties的语法,为child添加自有属性
属性名: {
value: 属性值,
writable:true,
enumerable: true,
configurable: false
},
... : {
... ...
}
})
其中做了3件事:1. 创建一个新对象。2. 让这个新对象继承father。3. 为这个新对象添加新属性。
4. 更换不想要的this为想要的对象(call、apply、bind)
只要函数调用时,this不是想要的,都可以用专门的函数更换为想要的对象!
-
临时: 在调用一个函数时,临时更换其中的this为指定的对象。函数调用后,恢复原样。
函数.call(替换this的对象, 实参值,... ...)
其中做了3件事: 1. 调用这个函数。2. 临时替换函数中的this为()中第一个实参对象。3. 将()中从第二个实参值开始的所有剩余实参值,传递给函数作为参数值。
如果传入函数的参数是放在数组中的,而函数却需要多个数值分别传入,就要用apply()代替call()
apply()做4件事: 比call()多一个功能,1. 调用这个函数。2. 临时替换函数中的this为()中第一个实参对象。3. 将一个数组整体,打散为单个数值,分别传入函数中。4. 将()中从第二个实参值开始的所有剩余实参值,传递给函数作为参数值。 -
永久: 为某一个对象,专门创建一个函数副本。其中永久绑定this为这个对象。
var 新函数=原函数.bind(替换this的对象)
其中做了2件事:1. 创建一个新函数副本,功能原函数一模一样。2. 将新函数副本中的this永久指向替换this的对象。
结果: 新函数() 时,不用再反复传入替换this的对象名。var 新函数=原函数.bind(替换this的对象, 实参值)
其中做了3件事:1. 创建一个新函数副本,功能原函数一模一样。2. 将新函数副本中的this永久指向替换this的对象。3. 将实参值,按顺序永久绑定到形参变量上。
结果: 新函数() 时,永久绑定的参数,不用再反复传值,只需要传剩余未绑定的参数值即可。
5. 数组函数:
-
1.判断: 判断数组内容是否符合要求。包含2种:
- 判断是否所有元素都符合要求
var bool=arr.every(function(value, i, arr){ //every会在每个元素上自动调用一次这个函数 //每次调用时: value: 会获得当前元素的值。i : 会获得当前元素的位置。arr : 会获得当前数组 return 判断条件 })
原理: every()会自动遍历数组中每个元素,每遍历一个元素就自动调用回调函数一次。调用时传入指定的实参值。回调函数会返回一个判断结果。如果任意一个元素上回调函数的判断结果返回false,整个every()就返回false。除非所有元素,经过回调函数的验证,都返回true,整个every()才返回true。
- 判断是否包含符合要求的元素
var bool=arr.some(function(value,i,arr){ return 判断条件 })
原理: 只要some碰到一个返回true的元素,整个some()就返回true。除非所有元素判断结果都为false,整个some()才返回false
-
2.遍历: 对数组中每个元素执行相同的操作
- arr.forEach(): 对数组中每个元素执行相同操作
arr.forEach( function(value,i,arr){ //forEach会自动在每个元素上调用一次回调函数 //每次调用时: value: 获得当前属性值。i : 获得当前位置。arr : 获得当前数组对象 } )
- arr.map() 依次取出原数组中每个元素,执行操作后,放入新数组中返回
var newArr=arr.map( //var newArr=[]; //for(var i=0;i<arr.length;i++){ function(value,i,arr){ return 新值 } //newArr[i]=新值 //} //return newArr; )
强调: 1. 返回新数组,所以必须用变量接住新数组。 2. 原数组中的元素值保持不变。
-
3.过滤和汇总:
- 过滤: 复制出数组中符合条件的元素,组成新数组返回。
var subArr=arr.filter( //var subArr=[]; //for(var i=0;i<arr.length;i++){ function(value,i,arr){ return 判断条件 } //只有判断条件为true的元素才会被加入新数组subArr中 //} //return subArr; )
- 汇总: 对数组中的元素内容进行统计,得出最终结论。以求和为例:
var sum=arr.reduce( function(prev, value, i, arr){ //prev是截止到当前位置之前的临时汇总值 return prev+value }, 起始值 )
ES6:
1. let:
代替var用于声明变量。
- var的缺点:1. 会被声明提前。2. 没有块级作用域。
- let的优点:1. 阻止了声明提前。2. 让程序块也变成了作用域。比如:
if(){... }else if(){... }else{ ... }
for/while(){ ... }
do{ ... }while;
原理:let其实就是一个匿名函数自调+改名
let a;
...
翻译:
(function(){
var _a=xxx;
...
})()
let小脾气:1. 同一作用域内,不允许同时let多个同名变量。2. 在当前作用域内,let a之前,不允许提前使用a变量
- let的兄弟: const:
const专门用于声明一个常量: 一旦初始化后,值不可改变!
其余特点和let是完全一样!
优化: 优先使用const,只有明确需要频繁改变的值,才用let。
因为所有const声明的常量,都集中存储在一个固定的区域中。js引擎查找变量时,优先查找常量区域。
2. 箭头(arrow)函数:
对一切匿名函数或回调函数的简化写法。所有匿名函数和回调函数都可以写成箭头函数
- 简化三步骤:
- 去掉function,在()和{}之间加=>
- 如果只有一个形参,可省略()
- 如果函数体只有一句话,可省略{}。如果唯一的这句话还是return则必须去掉 return。
强调: 如果去掉{}后,注意去掉语句后的分号!
- 箭头函数的独有特点:
- 函数内的this与函数外的this自动保持一致!
- 如果希望函数内的this和函数外的this保持一致时,就可转为箭头函数。
- 如果反而希望函数内外的this不一致,就不能用箭头函数,比如: 对象中的方法不能用=>
3. for of
简化遍历数组的一种方式
- 遍历总结:
- 遍历索引数组: 4种:
1.for(var i=0;i<arr.length;i++)
优点: 可随意控制遍历的方向,步调
2.arr.forEach()
普通顺序遍历时,对原数组进行操作时
3.arr.map()
保护原数组不变,返回修改后的新数组
4.for of
: 依次遍历数组中每个值
强调: 只能获得属性值,无法获得位置i。仅关心元素值,不关心位置时使用。 - 遍历关联数组和对象:只能用
for in
for in
不能用来遍历索引数组或类数组对象。
因为in查找范围不仅包含当前对象的所有下标,还会遍历父对象的所有可以遍历的成员
总结:
1.只要是数字下标的,都可用for循环和for of遍历。
2.只要是自定义名称下标的,都只能用for in遍历。 - 遍历索引数组: 4种:
4. 参数增强:
-
参数默认值(default): 即使没有传入实参值,形参变量也可提前定义一个备用值。 在定义函数时:
function fun(形参1, ... ,最后一个形参=默认值)
调用时,如果提供了最后一个实参值,则使用提供的实参值。如果没有提供最后一个实参值,则自动采用默认值为最后一个实参值。如果只有最后一个形参不确定,才可用默认值。如果多个形参都不确定,不能用默认值! -
剩余(rest)参数:代替arguments来实现重载效果。
arguments的3个缺点:- 不是纯正的数组类型,不能使用数组加的函数
- 只能获得所有实参值,不能有所选择
- 箭头函数中不能使用arguments。
只要用arguments的场景,都要用rest语法代替。 - 定义函数时:
function fun(形参1,……, ...数组名){}
- 将来调用时:
1.前几个有名称的形参,分别接住自己对应位置的实参值。
2.实参值列表中,前几个形参挑剩下的实参值,全都放入数组中。
rest优点:
1.数组名 是一个纯正的数组,数组加函数都可使用
2.仅接受剩余的实参值,不和之前的形参抢参数值,可以有所选择。
3.箭头函数也可以使用
-
打散数组参数: 将一个数组,打散成多个元素值,分别传给函数。如果函数定义时,需要多个参数值,而实参值却是放在一个数组中传来的,就可以打散。
- apply()可以打散数组为单个值,并传参。但pply()本职工作是替换this,顺便打散数组,但是很多打散数组的情况和this无关!
- 调用函数时: fun(…数组),会将数组拆成多个元素传给fun()
总结:
1.定义函数时,形参列表中的…表示收集剩余参数
2.调用函数时,实参列表中的…表示打散
5.解构: destruct
将巨大的对象和数组中成员,单独提取出来使用。从前只要访问对象的成员,都必须加"对象."前缀,太麻烦!只要从一个巨大的对象中提取成员单独使用时就用解构。
-
解构3种使用方式:
- 1.数组解构: 提取出数组中的个别元素,单独使用。 从前想使用数组中一个元素必须"数组名[下标]",繁琐,而且下标是无意义的,让程序的可读性变差。
下标对下标
var [变量1, 变量2,...]=数组 // 变量1=数组中[0]位置的值 // 变量2=数组中[1]位置的值 ... ...
- 2.对象解构: 提取出对象中的个别成员,单独使用。从前想使用对象中一个成员必须"对象."前缀,繁琐。
属性对属性
var {属性1:变量1, 属性2:变量2, ... }=obj // 变量1=obj[属性1]的值 // 变量2=obj[属性2]的值
对象解构和打散操作:
浅克隆一个对象: var obj2={…obj1}
合并两个对象: var obj={…obj1, …obj2}
等效于: var obj=Object.assign({},obj1,obj2)-
3.参数解构: 其实就是对象解构在传参时的应用。定义函数时,多个形参变量都不确定有没有,而且又要求实参值必须与形参变量对应时使用参数解构。
- 使用方式 2步:
1.定义函数时: 就要将所有形参变量都定义在一个对象结构中
2.调用函数时: 也必须传入一个对象,且对象中的属性名要和定义函数时,形参列表中的属性名保持一致!function fun({ // 配对 变量 属性1: 形参1 属性2: 形参2 ... : ... }){ //函数体 }
fun({ // 配对 实参值 属性1: 实参值1 属性2: 实参值2 ... : ... })
结果: 形参1 通过解构获得 实参值1
形参2 通过解构获得 实参值2问题: 如果调用函数时,没有提供指定属性名的实参值,是否会报错?
答: 不会报错!因为解构是试图访问原对象中的成员。而js中访问对象成员如果不存在,不会报错,而是返回undefined。也就是说,如果调用函数时,缺少部分实参值,也不会报错,而是对应的形参变量获得undefined而已。优点: 可随意省略任何一个实参值,都不会报错,还能保证其它几个实参值依然起作用。
- 使用方式 2步:
- 1.数组解构: 提取出数组中的个别元素,单独使用。 从前想使用数组中一个元素必须"数组名[下标]",繁琐,而且下标是无意义的,让程序的可读性变差。
-
总结: 如果有形参值将来调用时不确定:
- 如果只有最后一个形参值不确定: 默认值default
- 如果多个形参值,内容不确定,个数也不确定,但是不要求实参值与形参变量之间的对应关系: 剩余参数 rest …arr
- 如果多个形参值不确定,且要求实参值和形参变量必须对应: 参数解构
6.面向对象的简写:
-
1.对{}的简写:
- 所有的方法都可省略":function"
强调: 方法省略:function,和箭头函数原理完全不同。因为省略方法的:function,不影响this指向当前对象 - 如果属性的值来自于一个变量,而变量名刚好和属性名一致,只需要写一个即可!
var sname="Li Lei"; var friends=["明明","红红","兔兔"] var lilei={ sname:sname, //sname 既当属性名,又当变量 friends , sage : 11 }
- 如果属性名需要动态生成: 属性名的位置不能拼接字符串或使用模板字符串。我们可以将js表达式放在一个[]中。
var obj={ [js表达式]: 属性值 }
- 所有的方法都可省略":function"
7. class
集中保存一种类型的构造函数和原型对象方法的程序结构。旧js中,构造函数和原型对象方法是分着写的,不符合封装的概念。将来只要创建一种类型,并反复创建多个子对象时,都要先创建class。
-
创建class三步骤:
- 1.用class{}包裹构造函数和原型对象方法
- 2.构造函数名提升为类型名,写在class之后。
所有构造函数去掉function,并统一更名为constructor - 3.所有原型对象方法,都可以去掉"类型名.prototype前缀"和"=function"
强调: 直接定义在class中的属性,会自动成为每个子对象的自有属性,但是创建对象时,无法动态指定这种属性的属性值。所以,不建议使用。
如果必须要在原型对象中添加共有属性值,只能在class外,用旧方法: 类型名.prototype.共有属性=值使用class 和之前使用构造函数完全一样!
-
两种class之间的继承
当发现两种class之间存在部分相同的属性结构和方法定义时,都要定义一个父类型来集中管理两个class中相同部分的属性和方法定义。
实现分为2步:- 定义一个父类型,集中保存两个类型相同部分的属性定义和方法定义
1.父类型构造函数中包含子类型相同部分的属性定义
2.父类型class中包含子类型相同部分的方法定义 - 让子类型继承父类型
1.class 子类型 extends 父类型{ … }。extends 类似于 Object.setPrototypeOf的作用
2.子类型构造函数中,用supre(),调用父类型构造函数,和子类型构造函数一起强调: supre()必须写在子类型构造函数的开头,在所有子类型属性之前就要调用。因为: 这样可以保证,万一父类型和子类型刚巧有一个同名属性时,始终保证子类型的属性可以覆盖父类型的属性。而不允许父类型属性把子类型属性覆盖了。
- 定义一个父类型,集中保存两个类型相同部分的属性定义和方法定义
8. Promise
专门解决异步函数顺序执行的问题。异步函数的执行在主程序之外,主程序不会等待异步任务完成才执行。所以无法保证执行顺序。使用传统的回调函数方式实现顺序执行,会形成回调地狱。
-
创建new Promise()对象,包裹原异步函数的所有内容(原异步函数的内容不用变)。但是,new Promise()中需要保存的是原异步函数的所有代码段,所以,new Promise()中必须再嵌套一个function结构才能保存代码段。
new Promise(function(){ 原异步函数代码段 })
- 因为在Promise对象内,需要开门,才能自动调用下一个任务函数,所以new Promise赠送function()了一个开门函数:
new Promise(function(resolve){ ... })
- 在原函数异步任务执行后,开门
new Promise(function(resolve){ //原异步任务代码段 //异步函数最后执行的一句话后: resolve() })
- 最后,返回new Promise()对象到函数外部,用于和下一项任务对接。
return new Promise(function(resolve){ //原异步任务代码段 //异步函数最后执行的一句话后: resolve() })
- 因为在Promise对象内,需要开门,才能自动调用下一个任务函数,所以new Promise赠送function()了一个开门函数:
-
调用支持Promise的函数,并连接下一项任务
前一个任务函数().then(下一项任务函数)
- 调用前一项任务函数()做了两件事:
1.创建一个房间
2.让前一个异步任务在房间中执行
因为调用前一个任务函数,会返回Promise对象而每个Promise对象都带一个钩子.then()可以接下一个任务函数。但是暂不执行下一项任务函数
一旦前一个任务函数内调用了resolve()开门,则自动调用下一项任务函数。比如:function ming(){ //1. 用 return new Promise(function(resolve){ console.log("明起跑...") setTimeout(function(){ console.log("明到达终点!"); //当自己的任务执行后 resolve();//开门 },6000) }) } function hong(){ return new Promise(function(resolve){ console.log("红起跑...") setTimeout(function(){ console.log("红到达终点!"); //当自己的任务执行后 resolve();//开门 },6000) }) } ming()//2件事: // 1. 造房子 2. 让明在房子里起跑了 //return new Promise(...) .then(hong) //将然挂在ming的房子之后 //何时执行,必须等待明在房间中开门! //一旦明在房间中开门,.then会自动调用hong()
- 调用前一项任务函数()做了两件事:
-
错误处理: 一旦前一项任务中途出错,则下一项任务不再执行,而是进入错误处理流程。
- 其实,new Promise()赠送给函数两个门: resolve和reject,
return new Promise(function(resolve, reject){ ... })
- 如果异步操作中出错了,就不要开resolve()这个正常的门,应该开reject()这个门,同时,将错误原因,通过reject(“错误提示”),将错误原因传递给后续错误处理程序
- 只要在整个.then()链条的结尾添加一个.catch()就可接住前边任意一步任务中抛出的错误:
.then(...).catch(function(rejectMsg){ ... })
当之前任意一步函数中调用了reject(),都会停止整个流程,而是进入结尾的catch中执行函数。rejectMsg变量会接住reject(错误消息)参数中抛出的错误消息。比如:
function ming(){ return new Promise(function(resolve,reject){ console.log(`明起跑...`) setTimeout(function(){ if(Math.random()<0.6){ console.log("明到达终点!"); //当自己的任务执行后 resolve();//开门,并把参数棒给下一个函数 }else{ //一旦出错,开reject这扇门,直通最后的catch(),并传出错误消息 reject("吧唧,明摔倒了!") } },6000) }) } function hong(){ return new Promise(function(resolve,reject){ console.log(`红起跑...`) setTimeout(function(){ if(Math.random()<0.6){ console.log("红到达终点!"); //当自己的任务执行后 resolve();//开门,并把参数棒给下一个函数 }else{ //一旦出错,开reject这扇门,直通最后的catch(),并传出错误消息 reject("吧唧,红摔倒了!") } },6000) }) } ming() .then(hong) .catch(function(rejectMsg){ console.log(rejectMsg); console.log("集体退赛!") })
- 其实,new Promise()赠送给函数两个门: resolve和reject,
-
前一个任务向下一个任务传参: resolve()开门时,可以传入一个参数值。
这个参数值,可以自动传给.then()中下一个函数的第一个参数。在下一个函数中可以使用。比如:function ming(){ //1. 用 return new Promise(function(resolve,reject){ var bang="接力棒"; console.log(`明拿着 ${bang} 起跑...`) setTimeout(function(){ if(Math.random()<0.6){ console.log("明到达终点!"); //当自己的任务执行后 resolve(bang);//开门,并把参数棒给下一个函数 }else{ //一旦出错,开reject这扇门,直通最后的catch(),并传出错误消息 reject("吧唧,明摔倒了!") } },6000) }) } function hong(b1){ console.log(`然拿着 ${b1} 起跑...`) } ming().then(hong)
结果: hong(b1)中的形参b1接住了ming()中resolve(bang)开门时传出的bang的参数值
强调: resolve()这个函数,只能传入一个形参!如果有多个值需要传给下一个函数,可以将多个值放在一个数组或对象中传递:比如:
function 前一个函数(){ return new Promise(function(){ ... var obj={sname: "Li Lei", sage:11} resolve(obj); ... }) } ... function 下一个函数(obj){ obj接住的就是: {sname: "Li Lei", sage:11} }
到这里Es5、Es6的重要内容都包括了,你是否有和之前不一样的理解呢?我在这里只是简单列举说明了一下重要内容,有什么不理解的可以再搜索一下详细资料,或者可以交流一下。在此奉上个人博客