ES6基础

57 阅读14分钟

我正在参加「掘金·启航计划」

一、let与const

let、const的异同?
相同处:
都是只在声明所在的块级作用域有效,命令声明的常量也不会提升,存在暂时性死区。

区别:

const 指定的常量都不能改变,但是实际上他保证的是变量指向的那个内存地址所保存的数据不得改动,对于简单数据类型值就保存在变量指向的那个内存的地址,但是复合类型的数据(对象和数组)变量指向的是内存地址,保存的只是一个实际数据的指针。

var与function依旧属于顶层变量,let、const、class命令申明的全局变量不属于顶层对象的属性。

二、解构赋值

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。数组和对象内部都有Iterator接口,解构赋值会依次从这个接口获取值。

function f() {
  console.log('aaa');
}
let [x = f()] = [1]; // 默认值
let [x = 1, y = x] = []; // 引用解构赋值的其他变量
// 对象的解构
let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"
let { baz } = { foo: 'aaa', bar: 'bbb' };
baz // undefined
// 解构方法
// 例一
let { log, sin, cos } = Math;
// 例二
const { log } = console;
log('hello') // hello

解构赋值允许指定默认值,如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。默认值可以引用解构赋值的其他变量,但该变量必须已经声明。

对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。如果解构失败,变量的值等于undefined


三、字符串新增方法

  1. 新增了三种类似于indexof的方法:

includes():返回布尔值,表示是否找到了参数字符串。

startsWith():返回布尔值,表示参数字符串是否在原字符串头部。

endsWith():返回布尔值,表示参数字符串是否在原字符串尾部。

其中这三个都有第二个参数n,表示从第几个位置开始搜索,endsWith特殊是搜索前n个位置,其余是从第n个位置开始

  1. 新增repeat方法:
    返回一个新字符串,重复原字符串n次。不能为负数或infinity,会报错。
    x.repeat(3) // xxx
    如果输入的是字符串会会先转化为number,如果为-1~0,则会取整为0,0会返回空字符串,其余不符合条件的参数也会返回空字符串。
  2. **新增实例方法:padStart(),padEnd()
    **    ES2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全,一共接受两个参数,第一个参数是字符串补全生效的最大长度,第二个参数是用来补全的字符串。
    'x'.padStart(5, 'ab') // 'ababx'
    如果原字符串的长度,等于或大于最大长度,则字符串补全不生效,返回原字符串。如果用来补全的字符串与原字符串,两者的长度之和超过了最大长度,则会截去超出位数的补全字符串。如果省略第二个参数,默认使用空格补全长度。    
    有个例子是日期补全操作:
    '12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"

'09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12"

  1. 新增实例方法:trimStart(),trimEnd()     
    ES2019 对字符串实例新增了trimStart()trimEnd()这两个方法。它们的行为与trim()一致,trimStart()消除字符串头部的空格,trimEnd()消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。

四、数值的扩展****

**Number.EPSILON === Math.pow(2, -52) // true 2的-52次方
**    ES6 在Number对象上面,新增一个极小的常量Number.EPSILON。根据规格,它表示 1 与大于 1 的最小浮点数之间的差。是Javascript能表示的最小精度。误差如果小于这个值,就可以认为没有意义了,即不存在误差。
可以用来设置“能够接受的误差范围”

function withinErrorMargin (left, right) {
  return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2);
}
0.1 + 0.2 === 0.3 // false
withinErrorMargin(0.1 + 0.2, 0.3) // true
1.1 + 1.3 === 2.4 // false
withinErrorMargin(1.1 + 1.3, 2.4) // true

新增常用方法:

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

Math.sign():判断正负数

它会返回五种值。

1.参数为正数,返回+1;

2.参数为负数,返回-1;

3;参数为0,返回0;

4.参数为-0,返回-0;

5;其他值,返回NaN。
指数简化:V8 引擎的指数运算符与Math.pow的实现不相同,对于特别大的运算结果,两者会有细微的差异

ES2016 新增了一个指数运算符(**)。

2**2 === 4

2**3 === 8

223 === 2**(2**3) // 512

a **= 3 // a = aaa

数值问题一定会问精度问题

/**
 * 浮点数值精度计算
 * 方法一:计算两个数中最大的倍数,用最大的倍数算
 * 方法二:写一个方法单独计算每个数的整数与倍数,如下
 */
const float = function () {
  /**
   * 将一个浮点数转化为整数在进行运算
   */
  function toInteger(floatNum) {
    let ret = { times: 1, num: 0 }; // times代表倍数,num代表浮点数转化后的整数
    if (Number.isInteger(floatNum)) {
      ret.num = floatNum;
      return ret;
    }
    let strfi = floatNum.toString(); // 数值字符串化
    let n = strfi.split('.')[1].length;
    let times = Math.pow(10, n); // 10的len次方
    let intNum = Math.trunc(floatNum * times); // Math.trunc方法用于去除一个数的小数部分,返回整数部分。
    ret.times = times;
    ret.num = intNum;
    return ret;
  }
  /**
   * 精度计算,化为整数
   */
  function operation(a, b, op) {
    let o1 = toInteger(a);
    let o2 = toInteger(b);
    let n1 = o1.num;
    let n2 = o2.num;
    let t1 = o1.times;
    let t2 = o2.times;
    let max = t1 > t2 ? t1 : t2; // 选最大的倍数
    let result = null;
    switch (op) {
      case "add":
        if (t1 > t2) {
          result = (n1 + n2 * (t1 / t2)) / t1;
        } else if (t1 < t2) {
          result = (n1 * (t2 / t1) + n2) / t2;
        } else {
          result = (n1 + n2) / max;
        }
        console.log(result)
        return result;
      case "subtract":
        if (t1 > t2) {
          result = (n1 - n2 * (t1 / t2)) / t1;
        } else if (t1 < t2) {
          result = (n1 * (t2 / t1) - n2) / t2;
        } else {
          result = (n1 - n2) / max;
        }
        console.log(result)
        return result;
      case "multiply":
        result = (n1 * n2) / (t1 * t2);
        console.log(result)
        return result;
      case "divide":
        result = (n1 / n2) * (t2 / t1);
        console.log(result)
        return result;
      default:
        break;
    }
  }
  // 加减乘除的四个接口
  function add(a, b) {
    return operation(a, b, 'add');
  }
  function subtract(a, b) {
    return operation(a, b, 'subtract');
  }
  function multiply(a, b) {
    return operation(a, b, 'multiply');
  }
  function divide(a, b) {
    return operation(a, b, 'divide');
  }
  // 如果是整数,就不会保留小数点,只保留小数点超过指定位数的情况
  function toFixed(num, digits) {
    let times = Math.pow(10, digits);
    let des = num * times;
    return Math.trunc(des) / times;
  }
  // exports
  return {
    add,
    subtract,
    multiply,
    divide,
    toFixed
  }
}(); // 立即执行
float.add(0.07, 0.000001);
float.subtract(0.18, 0.2);
float.multiply(0.18, 0.2);
float.divide(0.1, 0.2);

五、函数的扩展

函数的默认值

函数的参数允许使用默认值,且内部存在默认值的时候,函数声明初始化的时候参数会形成一个单独的作用域,初始化结束,作用域会消失。

var x = 1;
function f(x, y = x) {
	console.log(y)
}
f(2) // 2
// 函数内部的变量影响不到参数
function f2(y = x) {
	var x = 2;
  console.log(y)
}
f2() // 1

箭头函数注意点:必考

  1. 函数体内的this对象,就是定义是 所在的对象 ,不是 运行时的对象

  2. 不可以当成构造函数,也就是不能使用new,否则会抛出错误。

  3. 不可以使用arguments对象,该对象在函数体内不存在,可以使用rest参数代替。

  4. 不可以使用yield命令,因此箭头函数不能用作Generator函数。

call()    apply()    bind()

在es5中都是可使用另一个对象的方法

call()与apply()用法相同,参数表现不同

bind()方法必须这样使用bind(...)()

且这些方法对箭头函数无效,因为箭头函数本身没有this

下面的函数,无论内部有多少箭头函数,内部的this都是指向foo

function foo() {
  return () => {
    return () => {
      return () => {
        console.log('id:', this.id);
      };
    };
  };
}
var f = foo.call({id: 1});
var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1

调用栈

先进后出结构(LIFO原则),用于存储在代码执行期间创建的所有执行上下文。

当JavaScript引擎首次读取脚本时,会创建一个全局执行上下文并将其Push到当前执行栈中。每当发生函数调用时,引擎都会为该函数创建一个新的执行上下文并Push到当前执行栈的栈顶。

引擎会运行执行上下文在执行栈栈顶的函数,根据LIFO规则,当此函数运行完成后,其对应的执行上下文将会从执行栈中Pop出,上下文控制权将转到当前执行栈的下一个执行上下文。

六、数组的扩展

Array.from(): 用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map),如果参数是一个真正的数组,Array.from会返回一个一模一样的新数组

const toArray = (() =>
  Array.from ? Array.from : obj => [].slice.call(obj) // 转化数组 兼容写法
)();

Array.of(): 用于将一组值,转换为数组。这个方法的主要目的,是弥补数组构造函数Array()的不足。因为参数个数的不同,会导致Array()的行为有差异。Array.of()基本上可以代替Array或new Array()

Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]
// 实现方法
function ArrayOf(){
  return [].slice.call(arguments);
}

find() 是用来查询是否有满足条件的数组成员,如果找到就返回数组成员,没有就返回undefined。

findIndex() 同上,区别是返回的是数组成员的下标,没有返回-1。

Array.prototype.includes

返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似,第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始。

indexOf方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。二是,它内部使用严格相等运算符(===)进行判断,这会导致对NaN的误判。

数组处理空位([ , , , ] undefined也算是有值)

ES5处理方法如下

  • forEach(), filter(), reduce(), every()some()都会跳过空位。
  • map()会跳过空位,但会保留这个值
  • join()toString()会将空位视为undefined,而undefinednull会被处理成空字符串。

ES6明确将空位变为undefined

Array.from(),...,会将空位转为undefined

for...of循环会遍历空位

七、对象的扩展

对象的enumerable属性,称为“可枚举性”,如果该属性为false,就表示某些操作会忽略当前属性。目前,有四个操作会忽略enumerable为false的属性。

  • for...in循环:只遍历对象自身的和继承的可枚举的属性。

  • Object.keys():返回对象自身的所有可枚举的属性的键名。

  • JSON.stringify():只串行化对象自身的可枚举的属性。

  • Object.assign(): 忽略enumerablefalse的属性,只拷贝对象自身的可枚举的属性。

super()关键字

this关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super,指向当前对象的原型对象。super关键字表示原型对象时,只能用在对象的方法之中,用其他地方都会报错。目前只有对象的简写方法,js认为是定义的是对象的方法。

链判断运算符(ECMA2020)

const firstName = message?.body?.user?.firstName || 'default';
// message如果存在就会获取其内部的body属性,如果body不存在就会结束获取

iterator.return?.() // 如果存在就立即执行的例子, 则直接返回undefined

// 一、短路机制
a?.[++x] // 等同于下面
a == null ? undefined : a[++x]
// 如果a是undefined或null,那么x不会进行递增运算。
// 也就是说,链判断运算符一旦为真,右侧的表达式就不再求值。

// 二、delete
delete a?.b
// 等同于
a == null ? undefined : delete a.b
// 如果a是undefined或null,会直接返回undefined,而不会进行delete运算。

// 三、括号的影响
(a?.b).c
// 等价于
(a == null ? undefined : a.b).c
// ?.对圆括号外部没有影响,不管a对象是否存在,圆括号后面的.c总是会执行。

// 四、报错场合
// 以下写法是禁止的 会报错
// 构造函数
new a?.()
new a?.b()

// 链判断运算符的右侧有模板字符串
a?.`{b}`
a?.b`{c}`

// 链判断运算符的左侧是 super
super?.()
super?.foo

// 链运算符用于赋值运算符左侧
a?.b = c

// 五、右侧不得为十进制数值

Null判断运算符

ES2020 引入了一个新的 Null 判断运算符??。它的行为类似||,但是只有运算符左侧的值为nullundefined时,才会返回右侧的值。这个运算符的一个目的,就是跟链判断运算符?.配合使用,为nullundefined的值设置默认值。

const animationDuration = response.settings?.animationDuration ?? 300;

如果多个逻辑运算符一起使用,必须用括号表明优先级,否则会报错。

lhs && middle ?? rhs // 会报错

(lhs && middle) ?? rhs; // 不报错

Object.is() 用于判断两个字段是否相等

== 会自动转换数据类型

=== NaN不会相等,+0 -0会相等

Object.is()解决了这些问题

// es5的实现方法
Object.defineProperty(Object, 'is', {
  value: function(x, y) {
    if (x === y) {
      // 针对+0 不等于 -0的情况
      return x !== 0 || 1 / x === 1 / y;
    }
    // 针对NaN的情况
    return x !== x && y !== y;
  },
  configurable: true,
  enumerable: false,
  writable: true
});

Object.assgin()

  1. 浅拷贝
  2. 同名属性会覆盖
  3. 可用来处理数组,覆盖
  4. 对象中取值函数的复制,不会复制函数,会直接把值复制过去

常见用途

  1. 为对象添加方法、属性

  2. 合并多个对象

  3. 为属性指定默认值

八、Symbol

let a = Symbol();

凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。

  1. Symbol 值不能与其他类型的值进行运算,会报错。
  2. Symbol 值可以显式转为字符串、布尔值,但是不能转化为数值。
  3. Symbol 值作为属性名时,该属性还是公开属性,不是私有属性。
  4. Symbol 值作为对象属性名时,不能用点运算符,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。如果s不放在方括号中,该属性的键名就是字符串s,而不是s所代表的那个 Symbol 值。
  5. Symbol 作为属性名,遍历对象的时候,该属性不会出现在for...in、for...of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。
  6. Reflect.ownKeys() 方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。
  7. Symbol 值作为键名,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法。
  8. Symbol.for() 与Symbol()这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境( 不管有没有在全局环境运行,都是全局环境 中供搜索,后者不会。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。Symbol.for()的这个全局登记特性,可以用在不同的 iframeservice worker 中取到同一个值。
  9. Symbol.keyFor() 方法返回一个已登记的 Symbol 类型值的key。
const sym = Symbol('foo');
sym.description // description方法可以返回描述 "foo"

Symbol.for("bar") === Symbol.for("bar") // true
Symbol("bar") === Symbol("bar") // false

let s1 = Symbol.for("foo"); // 登记
Symbol.keyFor(s1) // "foo"
let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined

九、Set与map

Set本身是一个构造函数,用来生成Set数据结构

    1. Set结构不会接受重复的值
    2. Set可以寄售一个数组,或者一个具有iterable结构的其他数据结构作为参数,用来初始化。
    3. 可用于数组去重
    4. Array.from方法可以把Set转化为数组
    5. 向Set加入值的时候不会发生类型转换,所以在Set内部5与"5"是不同的值,Set内部判断值是否相同使用的是"Same-value-zero equality",类似于 ===,区别在前者认为 NaN等于自身(NaN===NaN),后者不认为。结构中的对象都是不相等的。
// 去除数组重复成员,字符串也可以
[ ...Set([1, 2, 3, 1, 2]) ]
[ ...Set('abcab') ].join()

// 为什么可以用扩展运算符?因为...内部使用的是for of循环

Array.from(new Set(array)) // Array.from方法可以Set转化为数组

Set实例的方法:

    1. Set.prototype.constructor:构造函数,默认是Set函数。

    2. size:返回Set是来的成员总数。

    3. add:添加某个值,返回Set结构本身。

    4. delete:删除某个值,返回一个布尔值判断是否删除成功。

    5. has:判断是否有这个Set成员。

    6. clear:清除所有成员,没有返回值。

    7. (以下都是遍历) Set没有键名,或者说键值和键名是同一个值

    8. keys:返回键名

    9. values:返回键值

    10. entries:返回键值对

    11. forEach:使用回调函数遍历每个成员,使用方式和数组一样

// 使用Set可以容易的实现并集、交集、差集
let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);
let union = new Set([...a, ...b]) // 并
let intersect = new Set([...a].filter(x => b.has(x))) // 交
let difference = new Set([...a].filter(x => !b.has(x))) // 差

// 如果想在遍历中同步改变Set结构,目前没有直接的方法
let set = new Set([1, 2 ,3])
set = new Set([...set].map(x => x*2)) // 1
set = new Set(Array.from(set, x => x*2)) // 2

Map

Map结构提供"值--值"的对应,Object的键值只能是字符串,Map可以是一个各种类型的值无论是函数还是对象,是一种比Object更完善的hash结构。

如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如0和-0就是一个键,布尔值true和字符串true则是两个不同的键。另外,undefined和null也是两个不同的键。虽然NaN不严格相等于自身,但 Map 将其视为同一个键。

任何具有Iterator接口、每个成员都是一个双元素的数组的数据结构都可以当作Map构造函数的参数,也就是说Set和Map都可以用来生成新的Map。

const set = new Set([
  ['foo', 1],
  ['bar', 2]
]);
const m1 = new Map(set);
m1.get('foo') // 1

const m2 = new Map([['baz', 3]]);
const m3 = new Map(m2);
m3.get('baz') // 3

// Map构造函数接受数组作为参数,实际上执行的是下面的算法:
const items = [
  ['name', '张三'],
  ['title', 'Author']
];
const map = new Map();
items.forEach(
  ([key, value]) => map.set(key, value)
);

// Map转数组,数组转Map
const myMap = new Map()
	.set(true, 7)
	.set({foo: 3}, ['abc'])
[...myMap] // [[true, 7], [{foo: 3}, ['abc']]]
// 如果所有 Map 的键都是字符串,它可以无损地转为对象
// 对象转为 Map 可以通过Object.entries()
let obj = {"a":1, "b":2};
let map = new Map(Object.entries(obj));