ES6 新语法

一、变量

1. let

  1. let 新增了块级作用域

    • 每个代码块都可以有自己的作用域

    • 块级作用域的出现,使得匿名立即执行函数表达式(匿名 IIFE)不再必要了。

    • ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。

  2. 声明变量不会前置

  3. letconst 变量的暂时性死区 (Temporal Dead Zone)

    • 如果区块中存在 letconst 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错

    • 暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

  4. let 不允许在相同作用域内,重复声明同一个变量

  5. 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

  1. 声明常量。初始化时,必须给值

    • 对于引用变量,保证引用不变。

    • 本质: const 实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。

  2. const 的作用域与 let 命令相同:只在声明所在的块级作用域内有效。

  3. 如果真的想将对象冻结,应该使用 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 中,全局变量和顶层对象的属性逐渐脱钩

    • 使用 varfunction 定义的全局变量仍然是顶层对象的属性

    • 使用 let, const, class 定义的全局变量不再是顶层对象的属性

let a = 10;
console.log(window.a);  // undefined
let b = 20;
console.log(window.b);  // undefined

二、解构操作

1. 数组的解构赋值

  1. 等号左侧匹配等号右侧

  2. 不完全匹配:等号左侧部分匹配等号右侧。可成功

  3. 如果等号右边是一个 不可遍历的解构(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);
默认值
  1. 解构时,可为变量提供默认值

  2. 只有变量赋值严格等于(===) undefined 时,才会使用默认值

  3. 默认值可以引用其他解构的变量,但该变量必须事先声明

// 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. 基本应用,属性名和变量名必须一致,顺序可不一致

  2. 本质: 先找属性名,然后将属性值赋给对应变量

  3. 对象解构也可以使用默认值

  4. 由于数组也是个对象,所以也可以以对象方式进行解构

// 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. 数值和布尔值的解构赋值

  • 解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。

  • 解构赋值的规则是: 只要等号右边的值不是对象或数组,就先将其转为对象。

  • 由于 undefinednull 无法转为对象,所以对它们进行解构赋值,都会报错。

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. 与普通函数的区别

  1. 不绑定 this, arguments(用剩余参数...代替)

  2. 简化语法

4. 不适用场景

  1. 对象方法

  2. 构造函数

  3. 原型方法

四、扩展运算符 ...

// ... 运算符
// 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

  1. 模块文件 ./multi_module.js

    export function f1(){
        console.log('f1()');
    }
    ​
    export default function f2(){
        console.log('f2()');
    }
  2. 使用模块:./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 处理。

  3. 常用导出方式

    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};
  4. 常用导入方式:导入全部 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. 模块的加载顺序

  1. 默认情况下,模块脚本使用和添加了 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

  2. 常规的内联脚本会忽略 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

  3. Async 对内联、外部模块同样适用。快速下载的脚本会在慢速下载的脚本之前执行。

    <!-- 一旦获取了导入,就会执行此操作 -->
    <script async type="module">
        import {addTextToBody} from './utils.mjs';
        
        addTextToBody('Inline module executed.');
    </script>
    <!-- 一旦获取了脚本和它的导入,就会执行此操作 -->
    <script async type="module" src="1.mjs"></script>

7. 获取凭据

如果请求来自相同的源,大多数基于 CORSAPI 会发送凭据(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 提供了两种方式来解决上述问题,asyncdefer,这两个属性使得 script 都不会阻塞 DOM 的渲染。

1. defer(推迟)

  • 如果 script 标签设置了该属性,则浏览器会 异步 的下载该文件并且不会影响到后续 DOM 的渲染;

  • 如果有多个设置了 defer 的 script 标签存在,则会在全部加载完毕后,按照顺序执行所有的 script;

  • defer 脚本会在文档渲染完毕后,DOMContentLoaded 事件调用前执行。

2. async

  • async 的设置,会使得 script 脚本 异步 的加载 并在允许的情况 下执行

  • async 的执行,并不会按着 script 在页面中的顺序来执行,而是谁先加载完谁执行。 DOMContentLoaded 事件的触发并不受async脚本加载的影响,在脚本加载完之前,它可能就已经触发了

3. 最佳实践

  1. defer

    • 如果你的脚本代码依赖于页面中的 DOM 元素(文档是否解析完毕)

    • 或者被其他脚本文件依赖。

  2. async

    • 如果你的脚本并不关心页面中的 DOM 元素(文档是否解析完毕)

    • 并且也不会产生其他脚本需要的数据。

  3. 如果不太能确定的话,用 defer 总是会比 async 稳定。。。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值