一、变量
1. let
-
let
新增了块级作用域-
每个代码块都可以有自己的作用域
-
块级作用域的出现,使得匿名立即执行函数表达式(匿名 IIFE)不再必要了。
-
ES6
的块级作用域必须有大括号,如果没有大括号,JavaScript
引擎就认为不存在块级作用域。
-
-
声明变量不会前置
-
let
和const
变量的暂时性死区 (Temporal Dead Zone)-
如果区块中存在
let
和const
命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错 -
暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
-
-
let
不允许在相同作用域内,重复声明同一个变量 -
for 循环体中 let 的父子作用域
<script> // let a = 10 // { // let b = 20; // } // console.log('a = ' + a); // console.log('b = ' + b); // error // 2. 变量前置 // console.log(a); // var a = 10 // console.log(a); // console.log(a); // error // let a = 10 // console.log(a); // 3. 暂时性死区 // { // // x = 10; // ReferenceError // console.log(typeof x); // ReferenceError // let x = 3; // } // 4. 多次声明 // var a = 10; // let a = 10; // error // let a = 10; // let a = 20; // error // 5. 闭包 // var fucs = []; // for(var i = 0; i < 5; i++){ // fucs[i] = function(){ // return i; // }; // } // for(var i = 0; i < 5; i++){ // console.log(fucs[i]()); // } // 6. for 的特殊之处 for(let i = 0; i < 3; i++){ let i = "abc"; // 这里不是错误 console.log(i); } // 设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。 </script>
2. 块级作用域与函数
-
ES6
引入了块级作用域,明确允许在块级作用域之中声明函数。ES6
规定,块级作用域之中,函数声明语句的行为类似于let
,在块级作用域之外不可引用。 -
为了减轻因此产生的不兼容问题,
ES6
在附录里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式。-
允许在块级作用域内声明函数。
-
函数声明类似于
var
,即会提升到全局作用域或函数作用域的头部。 -
同时,函数声明还会提升到所在的块级作用域的头部。
-
注意,上面三条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作let处理。
// ES6 function f(){console.log("outer")} (function(){ if(false){ function f(){ console.log("inner"); } } f(); }()); // error // 相当于: function f(){console.log("outer")} (function(){ var f = undefined; if(false){ function f(){ console.log("inner"); } } f(); }()); // error // 此写法没有大括号,所以不存在块级作用域, // 而 let 只能出现在当前作用域的顶层,所以报错。 // if(true) let x = 1; //error
最佳实践
考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也 应该写成函数表达式,而不是函数声明语句。
3. const
-
声明常量。初始化时,必须给值
-
对于引用变量,保证引用不变。
-
本质:
const
实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。
-
-
const
的作用域与let
命令相同:只在声明所在的块级作用域内有效。 -
如果真的想将对象冻结,应该使用
Object.freeze
方法。
// 1. 必须初始化 // const a; // error // a = 10; // 2. 不能修改 // const a = 100; // a = 200; // error // 3. 引用不变,对象内容可变 // const s = {}; // s.name ='z3'; // right // console.log(s.name); // 4. 冷冻对象 const s = {}; Object.freeze(s); s.name = 'z3'; // 不起作用;严格模式下,error console.log(s); // {}
4. 顶层对象的属性
-
在
ES5
中,全局变量即是顶层对象的属性,这是设计的一大败笔 -
在
ES6
中,全局变量和顶层对象的属性逐渐脱钩-
使用
var
和function
定义的全局变量仍然是顶层对象的属性 -
使用
let
,const
,class
定义的全局变量不再是顶层对象的属性
-
let a = 10; console.log(window.a); // undefined let b = 20; console.log(window.b); // undefined
二、解构操作
1. 数组的解构赋值
-
等号左侧匹配等号右侧
-
不完全匹配:等号左侧部分匹配等号右侧。可成功
-
如果等号右边是一个 不可遍历的解构(Iterator 接口),则报错。
// 1. 数组基本解构 // let [a, b, c] = [1, 2, 3]; // console.log(a); // console.log(b); // console.log(c); // 2. 数组嵌套解构。只要左右模式匹配即可解构;如果解构不成功,变量的值就等于undefined。 // let [foo, [[bar], baz]] = [1, [[2], 3]]; // console.log(foo); // console.log(bar); // console.log(baz); // let [,, c] = [1, 2, 3]; // console.log(c); // let [head, ...tail] = [1, 2, 3, 4, 5]; // console.log(head); // console.log(tail); // 3. 解构失败 // let [x, y] = ['a']; // console.log(x); // console.log(y); // 4. 不完全解构 // let [x, y] = [1, 2, 3]; // console.log(x); // console.log(y); let [a, [b], d] = [1, [2, 3], 4]; console.log(a, b, d);
默认值
-
解构时,可为变量提供默认值
-
只有变量赋值严格等于(===)
undefined
时,才会使用默认值 -
默认值可以引用其他解构的变量,但该变量必须事先声明
// let [foo = true] = []; // console.log(foo) // let [x, y = 'b'] = ['a']; // console.log(x); // console.log(y); // let [x, y = 'b'] = ['a', undefined]; // console.log(x); // console.log(y); // 只有赋值严格等于 undefined 时,才使用默认值 // let [x = 1] = [undefined]; // console.log(x); // let [y = 2] = [null]; // console.log(y); // 默认值可以引用解构赋值的其他变量,但该变量必须已经声明。 // let [x = 2, y = x] = [8]; // console.log(x, y); let [x = y, y = 2] = []; console.log(x, y); // error
2. 对象的解构赋值
-
基本应用,属性名和变量名必须一致,顺序可不一致
-
本质: 先找属性名,然后将属性值赋给对应变量
-
对象解构也可以使用默认值
-
由于数组也是个对象,所以也可以以对象方式进行解构
// 1. 基本应用,属性名和变量名必须一致,顺序可不一致 // let {name, age} = {name : 'stone', age : 28}; // console.log(name, age); // 2. 本质:先找同名属性,再给对应变量赋值 // let {name : n, age : a} = {name : 'stone', age : 28}; // console.log(n, a); // console.log(name); // error // 示例 1 相当于 // let {name : name, age : age} = {name : 'stone', age : 28}; // console.log(name); // console.log(age); // 3. 对象解构的嵌套 let obj = { p : ['hello', {y : 'world'}] } let {p : [x, {y}]} = obj; console.log(x, y, p); // p 只是模式,不是变量,所以 error // 下面语法可认为对象解构了 2 次,一次是整体赋给 p;一次解构赋给 x, y let {p, p : [x, {y}]} = obj; console.log(x, y, p); // 4. 默认值 let {x : y = 3} = {}; console.log(y); let {x : y = 3} = {x : 5}; console.log(y); // 数组对象的解构 let arr = [1, 2, 3, 4, 5]; let {0 : first, [arr.length - 1] : last} = arr; console.log(first, last);
3. 字符串的解构赋值
字符串也可以解构赋值。这是因为,字符串被转换成了一个类似数组的对象。
let [a, b, c] = "xyz"; console.log(a, b, c); let {length} = 'xyz'; console.log(length);
4. 数值和布尔值的解构赋值
-
解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。
-
解构赋值的规则是: 只要等号右边的值不是对象或数组,就先将其转为对象。
-
由于
undefined
和null
无法转为对象,所以对它们进行解构赋值,都会报错。
let {toString : s} = 123; console.log(s === Number.prototype.toString); let {toString : bl} = false; console.log(bl === Boolean.prototype.toString); // let {prop : x} = undefined; // error // let {prop : y} = null; // error
5. 函数参数的解构赋值
// 既为 数组赋默认值,又为 x,y 赋默认值 function add([x = 0, y = 0] = []){ return x + y; } console.log(add([1, 2])); console.log(add([1])); console.log(add([])); console.log(add()); // 对象参数 function move({x = 0, y = 0} = {}){ return x + ', ' + y; } console.log(move({x : 10, y : 20})); console.log(move({x : 10})); console.log(move({})); console.log(move());
6. 圆括号的解构赋值
ES6 的规则是:只要有可能导致解构的歧义,就不得使用圆括号。 最佳实践:尽量不要在解构中使用圆括号 可以使用圆括号的情况只有一种:赋值语句的非模式部分
let i; [(i)] = [10]; console.log(i); let p; ({p : (j)} = {p : 20}); console.log(j);
7. 用途
// 1. 交换变量 let x = 1, y = 2; console.log(x, y); [y, x] = [x, y]; console.log(x, y); // 2. 从模块中导出成员 // 3. 从函数中返回多个值
三、箭头函数
1. 为什么需要箭头函数
2. 语法
# 1. (p1, p2, ...) => { 函数声明 } # 2. (p1, p2, ...) => 表达式 // 单条语句 # 相当于:(p1, p2, ...) => {return 表达式;} # 3. (单一参数) => {函数声明} 或 单一参数 =>{函数声明} # 4. () => {函数声明}
3. 与普通函数的区别
-
不绑定 this, arguments(用剩余参数
...
代替) -
简化语法
4. 不适用场景
-
对象方法
-
构造函数
-
原型方法
四、扩展运算符 ...
// ... 运算符 // 1. 将数组转为参数序列 let p = [9, 8, 7, 6]; console.log(...p); // 2. 接收不定项参数,接收为数组 function push(a, ...item){ a.push(...item); } let arr = []; push(arr, [1, 2, 3, 4]); console.log(arr); // 3. 字符串 console.log([...'hello']); let s = 'x\uD83D\uDE80y'; console.log(s); console.log(s.length); console.log([...s].length);
五、module
模块是为了打包重用 JS 代码!
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
有效的模块路径说明符必须符合下列条件之一:
-
一个完整的非相对
URL
,这样在将其传给new URL
(moduleSpecifier
) 的时候才不会报错。 -
以 / 开头的。
-
以 ./ 开头的。
-
以 ../ 开头的。
1. import & export
-
模块文件
./multi_module.js
export function f1(){ console.log('f1()'); } export default function f2(){ console.log('f2()'); }
-
使用模块:
./Testx_multiModule.html
// 1. 导入默认模块 import f from './multi_module.js'; f(); // 2. 导入具名模块 import {f1} from './multi_module.js'; f1(); // 3. 导出所有模块,默认模块一定要放在前面 import f2, {f1} from './multi_module.js'; f1(); f2(); // 4. 整体导出 import * as all from './multi_module.js'; all.f1() all.default()
注: 一定要指定
type="module"
,此时,浏览器就会将内联脚本或外部脚本作为ECMAScript module
处理。 -
常用导出方式
function sum(){ let total = 0; for(var i = 0; i < arguments.length; i++){ total += arguments[i]; } return total; } function multiply(){ let total = 1; for(var i = 0; i < arguments.length; i++){ total *= arguments[i]; } return total; } export default {sum, multiply};
-
常用导入方式:导入全部
default
<script type="module"> import all from './simple_module.js'; console.log(all.sum(1, 2, 3, 4)); console.log(all.multiply(1, 2, 3, 4)); </script>
2. 动态导入
<script> // 1. 动态导入所有 document.getElementById('btn') .addEventListener('click', ()=>{ import('./multi_module.js').then(m => { m.f1(); m.default(); }); }); // 2. 动态导入具名对象 document.getElementById('btn') .addEventListener('click', ()=>{ import('./multi_module.js').then(({f1}) => { f1(); }); }); // 3. 常用方式:在顶部定义导入函数 const loadModule = () => import('./multi_module.js'); document.getElementById('btn') .addEventListener('click', ()=>{ loadModule().then(m => { m.f1(); }); }); </script>
3. 使用 async/await动态导入
import()
语句返回的总是一个 Promise
,这意味着我们可以对它使用 async/await
。
<script type="module"> const loader = () => import('./simple_module.js'); document.getElementById("btn") .addEventListener('click', async() => { const m = await loader(); console.log(m.default.sum(1, 2, 3, 4)); }); </script>
4. 模块只执行一次
虽然可以一个模块可引入多次,但是它们却仅仅会执行一次。这同样适用于 HTML
中的脚本模块 - 特定 URL
的模块脚本每页只执行一次。
<!-- 1.mjs 仅执行一次 --> <script type="module" src="1.mjs"></script> <script type="module" src="1.mjs"></script> <script type="module"> import "./1.mjs"; </script> <!-- 然而,普通的脚本却执行多次 --> <script src="2.js"></script> <script src="2.js"></script>
5. 模块的跨域问题
与常规脚本不同,模块脚本(及其引入的内容)是通过 CORS
获取的。这就意味着跨域的模块脚本必须返回有效的 CORS
响应头 ,比如: Access-Control-Allow-Origin: *
。
// utils.js export function addTextToBody(text){ const div = document.createElement('div'); div.textContent = text; document.body.appendChild(div); } // <!-- 该脚本不会执行, 因为它不能通过 CORS 检查 --> <script type="module" src="https://….now.sh/no-cors"></script> <!-- 该脚本不会执行, 因为它引入的脚本之一不能通过 CORS 检查 --> <script type="module"> import 'https://….now.sh/no-cors'; addTextToBody("This will not execute."); </script> <!-- 该脚本会执行,因为它通过了 CORS 检查 --> <script type="module" src="https://….now.sh/cors"></script>
6. 模块的加载顺序
-
默认情况下,模块脚本使用和添加了
defer
的常规脚本相同的执行队列。<!-- 这个脚本的执行会晚于… --> <script type="module" src="1.mjs"></script> <!-- …这个脚本… --> <script src="2.js"></script> <!-- …但是会在这个脚本之前执行。 --> <script defer src="3.js"></script>
加载顺序:
2.js
->1.js
->3.js
-
常规的内联脚本会忽略
defer
,然而内联模块脚本却总是被延迟,无论它们有没有导入任何东西。<!-- 这个脚本的执行会晚于… --> <script type="module"> addTextToBody("Inline module executed"); </script> <!-- …这个脚本… --> <script src="1.js"></script> <!-- …和这个脚本… --> <script defer> addTextToBody("Inline script executed"); </script> <!-- …但是会在这个脚本之前执行。 --> <script defer src="2.js"></script>
加载顺序:
1.js
-> 内联脚本 -> 内联模块 ->2.js
-
Async
对内联、外部模块同样适用。快速下载的脚本会在慢速下载的脚本之前执行。<!-- 一旦获取了导入,就会执行此操作 --> <script async type="module"> import {addTextToBody} from './utils.mjs'; addTextToBody('Inline module executed.'); </script> <!-- 一旦获取了脚本和它的导入,就会执行此操作 --> <script async type="module" src="1.mjs"></script>
7. 获取凭据
如果请求来自相同的源,大多数基于 CORS
的 API
会发送凭据(cookie
等),但是 fetch()
和模块脚本却是例外的——除非您要求它们,否则它们不会发送凭据除。
-
可以通过添加
crossorigin
属性来向同源模块添加凭据 -
如果打算向其他的源也发送凭据,使用
crossorigin="use-credentials"
。注意其他源必须使用Access-Control-Allow-Credentials:true
的响应头来响应。
<!-- 携带凭据获取(cookie 等) --> <script src="1.js"></script> <!-- 不携带凭据获取 --> <script type="module" src="1.mjs"></script> <!-- 携带凭据获取 --> <script type="module" crossorigin src="1.mjs?"></script> <!-- 不携带凭据获取 --> <script type="module" crossorigin src="https://other-origin/1.mjs"></script> <!-- 携带凭据获取 --> <script type="module" crossorigin="use-credentials" src="https://other-origin/1.mjs?"></script>
8. 兼容不支持 module 的浏览器
支持 type=module
的浏览器会忽略属性为 nomodule
的脚本。这意味着您可以给支持模块的浏览器提供模块树,同时给其他浏览器提供一个降级版本。如下所示:
<!-- 支持 module 使用这个 --> <script type="module" src="module.mjs"></script> <!-- 不支持支持 module 使用这个 --> <script nomodule src="fallback.js"></script>
9. MIME 类型
不同于常规脚本,浏览器中,模块脚本必须是有效的 JavaScript MIME
类型中的一种类型,否则模块就不会执行。HTML
标准建议使用 text/javascript
。
六、script 标签中的 async 和 defer
script 标签用于加载脚本与执行脚本,在前端开发中可以说是非常重要的标签了。 直接使用 script 脚本的话,html 会按照顺序来加载并执行脚本,在脚本加载 & 执行的过程中,会阻塞后续的 DOM 渲染。
引入第三方脚本时,如果第三方服务商出现了一些小问题,比如延迟之类的,就会使得页面白屏。
script
提供了两种方式来解决上述问题,async
和 defer
,这两个属性使得 script
都不会阻塞 DOM
的渲染。
1. defer(推迟)
如果 script 标签设置了该属性,则浏览器会 异步 的下载该文件并且不会影响到后续 DOM 的渲染;
如果有多个设置了 defer 的 script 标签存在,则会在全部加载完毕后,按照顺序执行所有的 script;
defer 脚本会在文档渲染完毕后,DOMContentLoaded 事件调用前执行。
2. async
async 的设置,会使得 script 脚本 异步 的加载 并在允许的情况 下执行
async 的执行,并不会按着 script 在页面中的顺序来执行,而是谁先加载完谁执行。 DOMContentLoaded 事件的触发并不受async脚本加载的影响,在脚本加载完之前,它可能就已经触发了
3. 最佳实践
-
defer
-
如果你的脚本代码依赖于页面中的
DOM
元素(文档是否解析完毕) -
或者被其他脚本文件依赖。
-
-
async
-
如果你的脚本并不关心页面中的
DOM
元素(文档是否解析完毕) -
并且也不会产生其他脚本需要的数据。
-
-
如果不太能确定的话,用
defer
总是会比async
稳定。。。