es6(1)

参考资料:

开言

为什么要专门来记录一篇es6,而不是像之前一样以面试题的方式单独记录,主要是最近在项目中遇到一个问题,webpack中使用import二次导入失败,就是最开始加载的时候能正常显示,但是第二次加载就没用了。

问了老师,说是那个js没有export模块,我试着修改了一下,并且将所有import改为require,目前对它的理解仅仅是require能写进函数里,成功解决问题后,我想了解一下这里面是什么原理,结果查资料就看到commonJS和es6等,我基础不好,确实是被这些词绕的头晕,有见过有看过,但是具体是为什么会出现这些一概不知。

这就是我专门记录这个模块的原由,同时正好找到几份不错的资料,也算结合一下提升技能。记录的大部分都是总结,比较基础和浅显,可能会分比较多的章数,因为一些中间不太了解的知识点要另外去总结和搜索。

记录

es6简介

ES6全称ECMAScript 6.0,最开始就是针对JavaScript定制的,只是因为java名称权限以及体现中立性,所以采用的是标准化组织ECMA的前缀进行命名的。

ECMAScript 2015和ES6的关系,首先ES6的第一个版本是在2015年发布的,正式名称就是《ECMAScript 2015 标准》(简称 ES2015),在此之后每天6月发布这一年的新版本,每个版本通过年份标记,因此ES6既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。本书中提到 ES6 的地方,一般是指 ES2015 标准,但有时也是泛指“下一代 JavaScript 语言”

Babel转码器

Babel 是一个广泛使用的 ES6 转码器,可以将 ES6 代码转为 ES5 代码,从而在老版本的浏览器执行。这意味着,你可以用 ES6 的方式编写程序,又不用担心现有环境是否支持。比如写了一个箭头函数,使用转码器就会转为普通函数,这样就能在不支持箭头函数的js环境中执行了。

let和const

ES6 新增了let和const命令,用来声明变量。它们的用法类似于var,但是所声明的变量,只在let和const命令所在的代码块内有效,可以浅显理解为{}中,也就是说比如在一个循环体中,使用var声明的化,在循环结束后,循环体外依旧能打印出来最后一个循环数据,但是使用这两个块级变量就可以解决这个问题。

另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。下面代码正确运行,输出了 3 次abc。这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域(同一个作用域不可使用 let 重复声明同一个变量)

for (let i = 0; i < 3; i++) {
  let i = 'abc';
  console.log(i);
}
// abc
// abc
// abc

变量提升

var命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined,let和const必须在声明后使用,否则报错。

暂时性死区

在代码块内,使用let命令声明变量之前,该变量都是不可用的,这在语法上,称为“暂时性死区”。只要块级作用域内存在let命令,它所声明的变量就“绑定”这个区域,不再受外部的影响。

var tmp = 123;

if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}

上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。

在没有let之前,typeof运算符是百分之百安全的,永远不会报错,但在let出现后,在let声明前调用typeof判断就会导致抛出ReferenceError

块级作用域

ES6 允许块级作用域的任意嵌套,下面代码使用了一个五层的块级作用域,每一层都是一个单独的作用域。第四层作用域无法读取第五层作用域的内部变量

{{{{
  {let insane = 'Hello World'}
  console.log(insane); // 报错
}}}};

匿名IIFE

块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了

匿名立即执行函数表达式(IIFE)在JavaScript中常被用于创建一个独立的作用域,以避免变量污染全局作用域。通过这种方式,IIFE内部的变量和函数对外部是不可见的,这有助于封装和保护代码

在ES6之前,JavaScript只有全局作用域和函数作用域,这导致了一些场景下的不合理性,比如内层变量可能会覆盖外层变量,或者循环变量可能会泄漏为全局变量。ES6引入了块级作用域,它允许在代码块(由大括号{}包围的代码区域)内部声明变量,并且这些变量只在块级作用域内部有效,外部无法访问。

由于块级作用域提供了类似于IIFE的封装能力,但语法更为简洁和直观,因此它实际上成为了IIFE的一种更为方便和现代的替代方案。开发者现在可以直接在需要的地方使用块级作用域来声明变量,而无需再编写额外的IIFE函数。

// IIFE 写法
// 第一种
(function () {
  var tmp = ...;
  ...
}());
// 第二种
(function () {
  var tmp = ...;
  ...
})();


// 块级作用域写法
{
  let tmp = ...;
  ...
}

变量的解构赋值

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构。

以前为变量赋值只能指定值,es6允许写成下面这种形式,表示从数组中提取值按照对应的位置对变量进行赋值,本质上这种写法属于模式匹配,只要等号两边模式相同,左边的变量就会被赋予相应的值。

let [a, b, c] = [1, 2, 3];

解构不成功,变量的值就等于undefined。以下两种情况都属于解构不成功。

let [foo] = [];
let [bar, foo] = [1];

另一种是不完全解构,即等号左边的模式只匹配一部分等号右边的数组,这种情况下解构依然成功。但是如果等号的右边不是数组或者说不是可遍历的解构,就会报错。事实上只要某种数据结构具有Iterator接口,都可以采用数组形式的解构赋值。

let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};

默认值

解构赋值允许指定默认值,但是ES6内部使用严格运算符(===)判断一个位置是否有效,所以只有成员严格等于undefined,默认值才会生效。按照下面这段代码,null不等于undefined,所以默认值无效。

let [x = 1] = [undefined];
x // 1

let [x = 1] = [null];
x // null

如果默认值是一个表达式,那么这个表达式是惰性请求的,即只有在用到的时候才会求值。

按照下面这段代码来分析,let [x = f()] = [1]; 是一个解构赋值表达式,它尝试从右侧的数组 [1] 中解构出元素来赋值给左侧的变量 x。由于数组 [1] 的第一个元素(索引为0的元素)是 1,这个值被直接赋给了 x。由于 x 能够从数组 [1] 中成功获取到值(即 1),因此默认值 f() 不会被执行。这是因为解构赋值中的默认值表达式是惰性求值的:只有当对应的解构位置没有值(即 undefined)时,默认值表达式才会被求值

function f() {  
  console.log('aaa');  
}  
  
let [x = f()] = [1];

对象的解构赋值

数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。

下面的代码中,等号左边的两个变量的次序,与等号右边两个同名属性的次序不一致,但是对取值完全没有影响。第二个例子的变量没有对应的同名属性,导致取不到值,最后等于undefined。

let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar

如果变量名与属性名不一致,必须写成下面这样

let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"

let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l 

也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。下面代码中,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo

let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
foo // error: foo is not defined

注意点

下面代码中,因为 JavaScript 引擎会将{x}理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题

// 错误的写法
let x;
{x} = {x: 1};
// SyntaxError: syntax error

// 正确的写法
let x;
({x} = {x: 1});

由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构

let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3

字符串的解构赋值

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

let {length : len} = 'hello';
len // 5

数值和布尔值的解构赋值

在JavaScript中,原始类型(如数字、字符串、布尔值)在需要时被当作它们的包装对象(如Number、String、Boolean)来处理,这被称为装箱(boxing)。不过,在解构赋值中,这种装箱行为并不会直接创建一个新的对象实例,而是以一种特殊的方式访问这些原始类型对应对象原型上的方法。

解构赋值时,如果等号右边是数值和布尔值,则会先转为对象,由于undefinednull无法转为对象,所以对它们进行解构赋值,都会报错。下面代码中解析第一个,第二个同理。

  1. let {toString: s} = 123; 这行代码尝试从数字123(一个原始类型)中解构出toString属性。然而,123本身并没有属性,因为它是一个原始类型,不是对象。但是,JavaScript在解构时,会尝试将原始值视为一个对象,并尝试访问该对象的toString属性。
  2. 在这里,JavaScript实际上并没有为123创建一个新的Number对象实例。相反,它直接访问了Number.prototype上的toString方法。因此,解构赋值的结果s直接引用了Number.prototype.toString方法。
  3. 所以,s === Number.prototype.toString的结果是true,因为s确实是对Number.prototype.toString的引用
let {toString: s} = 123;
s === Number.prototype.toString // true

let {toString: s} = true;
s === Boolean.prototype.toString // true

let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError

函数参数的解构赋值

在这个函数中,{x = 0, y = 0} = {}是一个对象解构赋值,但这里的{}是函数的默认参数值,而不是解构的目标对象。解构赋值发生在函数参数被实际传入的对象上。如果没有传入任何对象(即undefined),则使用{}作为默认值进行解构,但这里的x = 0y = 0是在解构过程中为未定义的属性提供的默认值,而不是为整个对象提供的默认值。

  • move({x: 3, y: 8}); 传入的对象有xy属性,直接解构为x: 3, y: 8
  • move({x: 3}); 传入的对象只有x属性,解构时y属性未定义,因此使用默认值0
  • move({}); 传入的对象没有xy属性,因此都使用默认值0
  • move(); 没有传入任何对象,使用默认值{}进行解构,xy同样使用默认值0

{}作为默认参数值,它的主要作用是确保当没有提供任何参数给move函数时,解构赋值操作有一个对象可以操作,从而避免抛出错误。这个默认对象{}本身不包含x或y属性,但解构赋值中的x = 0和y = 0确保了即使{}中没有这些属性,x和y也会被赋予默认值0。即{}作为默认参数值确保了即使在没有提供任何参数的情况下,函数也能正常运行而不会抛出错误。

    function move1({ x = 0, y = 0 } = {}) {
      console.log([x, y]) ;
    }

    move1({ x: 3, y: 8 }); // [3, 8]
    move1({ x: 3 }); // [3, 0]
    move1({}); // [0, 0]
    move1(); // [0, 0]

move2 的默认值是在参数对象整体上设置的,也就是说,只有当没有传入参数时,整个参数对象才会使用默认值 { x: 0, y: 0 }。如果传入了一个对象 {},即使该对象没有 x 或 y,它也不会触发默认值 0,因为解构后的值是 undefined

  • move2({ x: 3, y: 8 }); 传入了完整的对象 { x: 3, y: 8 },所以输出 [3, 8]
  • move2({ x: 3 }); 传入了 { x: 3 },但是没有传入 y,所以 y 的值是 undefined,输出 [3, undefined]
  • move2({}); 传入了一个空对象 {},由于 x 和 y 都没有提供,它们的值都是 undefined,所以输出 [undefined, undefined]
  • move2(); 没有传入任何参数,所以会使用默认值 { x: 0, y: 0 },输出 [0, 0]

move2() 没有传入任何参数,因此会使用函数定义中提供的默认值 { x: 0, y: 0 } 作为整个对象的默认值。move2({}) 传入了一个空对象 {},虽然该对象没有 x 或 y 属性,但因为传递了对象,默认值 { x: 0, y: 0 } 不会被使用。解构时没有找到属性的值,结果是 undefined

    function move2({ x, y } = { x: 0, y: 0 }) {
      console.log([x, y]) ;
    }

    move2({ x: 3, y: 8 }); // [3, 8]
    move2({ x: 3 }); // [3, undefined]
    move2({}); // [undefined, undefined]
    move2(); // [0, 0]

用途

交换变量的值

let x = 1;
let y = 2;

[x, y] = [y, x];

从函数返回多个值

// 返回一个数组

function example() {
  return [1, 2, 3];
}
let [a, b, c] = example();

// 返回一个对象

function example() {
  return {
    foo: 1,
    bar: 2
  };
}
let { foo, bar } = example();

提取 JSON 数据

let jsonData = {
  id: 42,
  status: "OK",
  data: [867, 5309]
};

let { id, status, data: number } = jsonData;

console.log(id, status, number);
// 42, "OK", [867, 5309]

遍历 Map 结构

任何部署了 Iterator 接口的对象,都可以用for...of循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便

const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');

for (let [key, value] of map) {
  console.log(key + " is " + value);
}


// 获取键名
for (let [key] of map) {
  // ...
}

// 获取键值
for (let [,value] of map) {
  // ...
}

字符串的扩展

ES6 加强了对 Unicode 的支持,允许采用\uxxxx形式表示一个字符,其中xxxx表示字符的 Unicode 码点,但是,这种表示法只限于码点在\u0000~\uFFFF之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。

ES6 对这一点做出了改进,只要将码点放入大括号,就能正确解读该字符

"\u0061"
// "a"


"\uD842\uDFB7"
// "𠮷"


"\u20BB7"
// " 7"

"\u{20BB7}"
// "𠮷"

"\u{41}\u{42}\u{43}"
// "ABC"

let hello = 123;
hell\u{6F} // 123

'\u{1F680}' === '\uD83D\uDE80'
// true

字符串遍历器接口

ES6 为字符串添加了遍历器接口,使得字符串可以被for...of循环遍历,除了遍历字符串,这个遍历器最大的优点是可以识别大于0xFFFF的码点,传统的for循环无法识别这样的码点。

for (let codePoint of 'foo') {
  console.log(codePoint)
}
// "f"
// "o"
// "o"

let text = String.fromCodePoint(0x20BB7);

for (let i = 0; i < text.length; i++) {
  console.log(text[i]);
}
// " "
// " "

for (let i of text) {
  console.log(i);
}
// "𠮷"

模版字符串

模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量

// 普通字符串
`In JavaScript '\n' is a line-feed.`

// 多行字符串
`In JavaScript this is
 not legal.`

console.log(`string text line 1
string text line 2`);

// 字符串中嵌入变量
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`

如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中,可以使用trim方法消除

$('#list').html(`
<ul>
  <li>first</li>
  <li>second</li>
</ul>
`.trim());

大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。还能调用函数,如果大括号中的值不是字符串,将按照一般的规则转为字符串。比如,大括号中是一个对象,将默认调用对象的toString方法

  • 14
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值