ECMAScript 6 入门---总结01
1.ECMAScript 6简介
Babel:Babel是一个工具链,主要用于将ECMAScript 2015+版本的代码转换为向后兼容的JavaScript语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
一般用来将ES6代码转换为ES5代码,从而在现有环境执行
2.let和const命令
let和const:不存在变量提升(即必须先声明后使用)、暂时性死区、不允许重复声明
块级作用域:考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。
// 块级作用域内部的函数声明语句,建议不要使用 { let a = 'secret'; function f() { return a; } } // 块级作用域内部,优先使用函数表达式 { let a = 'secret'; let f = function () { return a; }; }
const:const实际上保证的,并不是变量的值不得改动,而是变量指向的哪个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量;但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针式固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。如果真的想将对象冻结,则应该使用Object.freeze方法
const foo = {}; // 为 foo 添加一个属性,可以成功 foo.prop = 123; foo.prop // 123 // 将 foo 指向另一个对象,就会报错 foo = {}; // TypeError: "foo" is read-only const a = []; a.push('Hello'); // 可执行 a.length = 0; // 可执行 a = ['Dave']; // 报错
顶层对象:浏览器环境中指的是window对象,在Node指的是global对象。
从ES6开始,全局变量将逐步与顶层对象的属性脱钩,var和function声明的全局变量仍然是顶层对象的属性;let、const、class声明的全局变量不属于顶层对象的属性。
如何让在各种环境下,都取到顶层对象
3.变量的解构赋值
数组的解构赋值:方括号
对象的解构赋值:大括号
数组和对象的解构赋值都可以使用默认值
常用:交换变量的值、从函数返回多个值(方便)、定义函数参数、提取JSON数据
4.字符串的扩展
模板字符串(反引号)、模板字符串中嵌入变量或方法(使用$)
5.字符串的新增方法
includes():返回布尔值,表示是否找到了参数字符串
startsWith():返回布尔值,表示参数字符串是否在原字符串的头部
endsWith():
repeat():返回一个字符串,表示将原字符串重复n次
padStart():用于头部补全
padEnd():
trimStart():消除字符串头部的空格
trimEnd():
matchAll():返回一个正则表达式在当前字符串的所有匹配
6.正则的扩展
7.数值的扩展
二进制(0b或0B)、八进制(0o或0O)、新增17种Math对象的静态方法、指数运算符(**)、BigInt(后缀加n)
8.函数的扩展
函数参数的默认值(参数默认值只能设置在尾部)
rest参数(...变量名):用于获取函数的多余参数;rest参数之后不能再有其他参数
function add(...values) { let sum = 0; for (var val of values) { sum += val; } return sum; } add(2, 5, 3) // 10
规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显示设定为严格模式,否则会报错
函数的那么属性,返回该函数的函数名:
function foo() {} foo.name // "foo"
箭头函数:箭头函数默认绑定外层this,实际原因是它根本没有自己的this导致内部的this就是外层代码块的this,而因为它没有this,所以不能用作构造函数。同时,因为没有自己的this,不能使用call()、apply()、bind()这些方法来改变this的指向
箭头函数可以多层嵌套
尾调用优化:尾调用是指某个函数的最后一步是调用另一个函数
// 尾调用 function f(x){ return g(x); } // 非尾调用 // 情况一 function f(x){ let y = g(x); return y; } // 情况二 function f(x){ return g(x) + 1; } // 情况三 function f(x){ g(x); }
尾调用不一定出现在函数尾部,只要是最后一步操作即可。
function f(x) { if (x > 0) { return m(x) } return n(x); }
函数调用会在内存形成一个“调用记录”,又称“调用帧”,保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧就形成一个调用栈。
由于尾调用是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。
function f() { let m = 1; let n = 2; return g(m + n); } f(); // 等同于 function f() { return g(3); } f(); // 等同于 g(3);
这就是尾调用优化,即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,将大大节省内存。
目前只有Safari浏览器支持尾调用优化,Chrome和Firefox都不支持
尾递归:函数调用自身,称为递归。如果尾调用自身,就称为尾递归。
递归非常耗费内存,因为同时需要保存成千上百个调用帧,很容易发生栈溢出错误。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生栈溢出错误
// 未用尾递归 function factorial(n) { if (n === 1) return 1; return n * factorial(n - 1); } factorial(5) // 120 // 使用尾递归 function factorial(n, total) { if (n === 1) return total; return factorial(n - 1, n * total); } factorial(5, 1) // 120
如上例,未使用尾递归计算n的阶乘,最多需要保存n个调用记录,复杂度O(n);如果改写成尾递归,只保留一个调用记录,复杂度O(1)
// 未使用尾递归 function Fibonacci (n) { if ( n <= 1 ) {return 1}; return Fibonacci(n - 1) + Fibonacci(n - 2); } Fibonacci(10) // 89 Fibonacci(100) // 超时 Fibonacci(500) // 超时 // 使用尾递归 function Fibonacci2 (n , ac1 = 1 , ac2 = 1) { if( n <= 1 ) {return ac2}; return Fibonacci2 (n - 1, ac2, ac1 + ac2); } Fibonacci2(100) // 573147844013817200000 Fibonacci2(1000) // 7.0330367711422765e+208 Fibonacci2(10000) // Infinity
可见尾调用优化对递归操作意义重大。ES6第一次明确规定,所有ECMAScript的实现都必须部署尾调用优化,即ES6中只要使用尾递归,就不会发生栈溢出或层层递归造成的超时现象,相对节省内存
尾递归的实现,需要改写递归函数。往往把所有用到的内部变量改写成函数的参数。但是缺点是函数的意思不够直观,很难一眼看出来函数的作用。一般有两个方法来解决:
方法一是在尾递归函数之外,再提供一个正常形式的函数
// 尾递归函数 function tailFactorial(n, total) { if (n === 1) return total; return tailFactorial(n - 1, n * total); } // 正常函数 function factorial(n) { return tailFactorial(n, 1); } factorial(5) // 120
方法二是柯里化,意思是将多参的函数转换成单参数的形式
function currying(fn, n) { return function (m) { return fn.call(this, m, n); }; } function tailFactorial(n, total) { if (n === 1) return total; return tailFactorial(n - 1, n * total); } // 将尾递归函数tailFactorial变为只接受一个参数的递归函数 const factorial = currying(tailFactorial, 1); factorial(5) // 120
方法三是使用ES6的函数默认值
// 参数total有默认值1,所以调用时不用提供这个值 function factorial(n, total = 1) { if (n === 1) return total; return factorial(n - 1, n * total); } factorial(5) // 120
递归本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令,所有的循环都用递归实现,这就是为什么尾递归对这些语言及其重要。循环可以用递归代替,而一旦使用递归,就最好使用尾递归。
ES6的尾调用优化只在严格模式下开启生效
ES6的toString()方法:toString()返回函数代码本身,以前会省略注释和空格
ES6的catch:允许catch语句省略参数
9.数组的扩展
扩展运算符(...):将一个数组转为用逗号分隔的参数序列。主要用于函数调用
用途:替代函数的apply方法、复制数组(深拷贝)、合并数组(浅拷贝)
Array.from():用于将两类对象转为真正的数组,类似数组的对象(array-like object)和可遍历(iterable)的对象(包括ES6新增的数据结构Set和Map)
Array.of()方法用于将一组值转换为数组
copyWithin():在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。
find()和findIndex():用于找出第一个符合条件的数组成员。参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员
fill():使用给定值填充一个数组,常用于空数组的初始化。注意,如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象
ES6提供三个新的方法-----entries()、keys()、和values()------用于遍历数组。它们都返回一个Iterator对象,可以用for...of循环进行遍历。唯一的区别是keys()是对键名的遍历、values()是对键值的遍历、entries()是对键值对的遍历。如果不使用for...of循环,可以手动调用Iterator对象的next方法,进行遍历。
includes():返回一个布尔值,表示某个数组是否包含给定的值。之前常用indexOf方法,检查是否包含某个值,但是这样做有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。二是,它内部使用严格相等运算符(===)进行判断,这会导致对NaN的误判;而includes()不会出现这些问题
flat()、faltMap():用于将数组中额数组成员拉平,变成一维数组
Array.prototype.sort()的排序稳定性:ES6明确规定,该排序算法必须稳定。常见的排序算法中,插入排序、合并排序、冒泡排序等都是稳定的,堆排序、快速排序是不稳定的。不稳定排序的主要缺点是,多重排序时可能会产生问题。假设有一个姓和名的列表,要求按照“姓氏为主要关键字,名字为次要关键字”进行排序。开发者可能会先按名字排序,再按姓氏进行排序。如果排序算法是稳定的,这样就可以达到“先姓氏,后名字”的排序效果。如果是不稳定的,就不行。
10.对象的扩展
方法的name属性:返回函数名。对象方法也是函数,因此也有name属性
两种特殊情况:1.bind方法创造的函数,name属性返回bound加上原函数的名字;2.Function构造函数创造的函数,name属性返回anonymous
属性的可枚举型和遍历:对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为,Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象
let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
// {
// value: 123,
// writable: true,
// enumerable: true,
// configurable: true
// }
描述对象的enumerable属性,称为可枚举性,如果该属性为false,就表示某些操作会忽略当前属性。ES6规定,所有Class的原型的方法都是不可枚举的。总的来说,操作中引入继承的属性会让问题复杂化,大多数时候,我们只关心对象自身的属性。所以,尽量不要用for...in循环,而用Obiect.keys()代替。
ES6一共有5种方法可以遍历对象的属性
1.for...in:遍历对象自身的和继承的可枚举属性(不含Symbol属性)
2.Object.keys(obj):Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性)的键名
3.Object.getOwnPropertyNames(obj):返回一个数组,包含对象自身的所有属性(不含Symbol属性,但是包括不可枚举属性)的键名
4.Object.getOwnPropertySymbols(obj):返回一个数组,包含对象自身的所有Symbol属性的键名
5.Reflect.ownKeys(obj):返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是Symbol或字符串,也不管是否可枚举
以上的5种方法遍历对象的键名,都遵守同样的属性遍历的次序规则
-首先遍历所有数值键,按照数值升序排列
-其次遍历搜友字符串键,按照加入时间升序排列
-最后遍历所有Symbol键,按照加入时间升序排列
super关键字:this关键字总是指向函数所在的当前对象(我理解的是指向调用该方法的对象),ES6又新增了另一个类似的关键字super,指向当前对象的原型对象
const proto = { foo: 'hello' }; const obj = { foo: 'world', find() { return super.foo; } }; Object.setPrototypeOf(obj, proto); obj.find() // "hello"
注意,super关键字表示原型对象时,只能用在对象的方法中,用在其他地方都会报错(目前,只有对象方法的简写法可以让JavaScript引擎确认,定义的是对象的方法)
// 报错 // super用在了属性里面 const obj = { foo: super.foo } // 报错 // super用在了一个函数里,然后赋值给foo属性 const obj = { foo: () => super.foo } // 报错 // super用在了一个函数里,然后赋值给foo属性 const obj = { foo: function () { return super.foo } }
链判断运算符:编程实务中,如果读取对象内部的某个属性,往往需要判断以下该对象是否存在。
// 比如,要读取message.body.user.firstName,安全的写法是写成下面这样: // 错误的写法 const firstName = message.body.user.firstName; // 正确的写法 const firstName = (message && message.body && message.body.user && message.body.user.firstName) || 'default'; //三元运算符?:也常用于判断对象是否存在 const fooInput = myForm.querySelector('input[name=foo]') const fooValue = fooInput ? fooInput.value : undefined
这样层层判断非常麻烦,因此ES6引入了链判断运算符(?.),直接在链式调用的时候判断,左侧的对象是否为null或undefined。如果是的,就不再往下运算,而是返回undefined。对于那些可能没有实现的方法,这个运算符尤其有用
const firstName = message?.body?.user?.firstName || 'default'; const fooValue = myForm.querySelector('input[name=foo]')?.value // 判断对象方法是否存在,如果存在就立即执行,否则直接返回undefined,不再执行?.后面的部分 iterator.return?.()
机制:1.短路机制:只要不满足条件,就不再往下执行。即链式运算符一旦为真,右侧的表达式就不再求值
2.以下写法是禁止的,会报错
// 构造函数 new a?.() new a?.b() // 链判断运算符的右侧有模板字符串 a?.`{b}` a?.b`{c}` // 链判断运算符的左侧是 super super?.() super?.foo // 链运算符用于赋值运算符左侧 a?.b = c
3.右侧不得为十进制数值
Null判断运算符:读取对象属性的时候,如果某个属性的值是null或undefined,有时需要为他们指定默认值,常见的作法是通过||运算符指定默认值。但是当属性值为空字符串或false或0时,默认值也会生效。为避免这种情况,ES6引入了一个新的Null判断运算符??,它的行为类似||,但是只有在运算符左侧的值为null或undefined时,才会返回右侧的值。可以和链判断运算符?.配合使用。
const headerText = response.settings.headerText ?? 'Hello, world!'; const animationDuration = response.settings.animationDuration ?? 300; const showSplashScreen = response.settings.showSplashScreen ?? true;
??有一个运算优先级问题,它与&&和||的优先级孰高孰低。现在的规则是,如果有多个逻辑运算符一起使用,必须使用括号表明优先级,否则会报错。
11.对象的新增方法
Obiect.is():ES5比较两个值是否相等,只有两个运算符:相等运算符(==)和严格相等运算符(===)。它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0。JavaScript缺乏一种运算,在所有环境中,只要两个值是一样的,它们就应该相等。Obiect.is就是ES6用来解决这个问题的方法。它用来比较两个值是否严格相等,与===的行为基本一致,不同之处只有两个:一是+0不等于-0,二是NaN等于自身
Object.assign():用于对象的合并,将源对象的所有可枚举属性,复制到目标对象。拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable:false)。属性名为Symbol的属性,也会被Object.assign()拷贝。该方法实行的是浅拷贝,而不是深拷贝。而且一旦遇到同名属性,它的处理方法是替换而不是添加
常见用途:为对象添加属性、为对象添加方法、克隆对象、合并多个对象、为属性指定默认值
Object.getOwnPropertyDescriptors():ES6新增,返回指定对象所有自身属性(非继承属性)的描述对象。
原型对象相关方法:
__proto__属性:用来读取或设置当前对象的原型对象(prototype)。但是,无论是从语义的角度,还是从兼容性的角度,都不要使用这个属性,而是使用下面的
Object.setPrototypeOf():作用与__proto__相同,用来设置一个对象的原型对象,返回参数对象本身。它是ES6正式推荐的设置原型对象的方法。
Object.getPrototypeOf():与Object.setPrototypeOf(方法配套,用于读取一个对象的原型对象
Object.keys():返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名
Object.values():返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历属性的键值
Object.entries():返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历属性的键值对数组
Object.fromEntries():Object.entries()的逆操作,用于将一个键值对数组转换为对象,特别适合Map结构转换为对象。