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结构转换为对象。

 

  

posted @ 2020-04-18 11:25  ajjoker  阅读(106)  评论(0)    收藏  举报