JS&ES6学习笔记(持续更新)

ES6学习笔记(2019.7.29)

目录

letconst

let

let 基本用法

  • let声明的变量只在它所在的代码块(花括号)有效。

    {
    let a = 10;
    var b = 1;
    }
    a // ReferenceError: a is not defined.
    b
  • for循环的计数器,合适使用let命令, i用var命令声明的话,在全局范围内都有效。

  • 另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。

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

let 不存在变量提升

暂时性死区

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

    var tmp = 123;
    
    if (true) {
    tmp = 'abc'; // ReferenceError
    let tmp;
    } typeof x; // ReferenceError (typeof不再是一个百分之百安全的操作)
    let x;
    function bar(x = y, y = 2) {
    return [x, y];
    } bar(); // 报错

调用bar函数之所以报错(某些实现可能不报错),是因为参数x默认值等于另一个参数y,而此时y还没有声明,属于“死区”。如果y的默认值是x,就不会报错.

不允许重复声明

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

// 报错
function func() {
let a = 10;
var a = 1;
} // 报错
function func() {
let a = 10;
let a = 1;
} //因此,不能在函数内部重新声明参数。 function func(arg) {
let arg;
}
func() // 报错 function func(arg) {
{
let arg;
}
}
func() // 不报错

块级作用域

级作用域

ES5 只有全局作用域和函数作用域,没有块级作用域,带来很多不合理的场景:

  1. 内层变量可能会覆盖外层变量,变量提升,导致内层的tmp变量覆盖了外层的tmp变量。
var tmp = new Date();

function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world';
}
}
f(); // undefined
  1. 计数的循环变量泄露为全局变量。
var s = 'hello';

for (var i = 0; i < s.length; i++) {
console.log(s[i]);
} console.log(i); // 5
ES6的块级作用域
  • 外层代码块不受内层代码块的影响。如果两次都使用var定义变量n,最后输出的值才是 10。
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}
  • ES6 允许块级作用域的任意嵌套,第四层作用域无法读取第五层作用域的内部变量。
{{{{
{let insane = 'Hello World'}
console.log(insane); // 报错
}}}};
  • 内层作用域可以定义外层作用域的同名变量。
{{{{
let insane = 'Hello World';
{let insane = 'Hello World'}
}}}};
  • 块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了。
// IIFE 写法
(function () {
var tmp = ...;
...
}()); // 块级作用域写法
{
let tmp = ...;
块级作用域与函数声明
  • ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。
// 情况一
if (true) {
function f() {}
} // 情况二
try {
function f() {}
} catch(e) {
// ...
}

上面两种函数声明,根据 ES5 的规定都是非法的。

但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。

  • ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。
function f() { console.log('I am outside!'); }

(function () {
if (false) {
// 重复声明一次函数f
function f() { console.log('I am inside!'); }
} f();
}());

上面代码在 ES5 中运行,会得到“I am inside!”,因为在if内声明的函数f会被提升到函数头部,实际运行的代码如下。

// ES5 环境
function f() { console.log('I am outside!'); } (function () {
function f() { console.log('I am inside!'); }
if (false) {
}
f();
}());

ES6 理论上会得到“I am outside!”。因为块级作用域内声明的函数类似于let,对作用域之外没有影响。但是,在 ES6 浏览器中运行一下上面的代码,是会报错的,改变了块级作用域内声明的函数的处理规则,显然会对老代码产生很大影响。为了减轻因此产生的不兼容问题,ES6 在附录 B里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式

  • 允许在块级作用域内声明函数。
  • 函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
  • 同时,函数声明还会提升到所在的块级作用域的头部。

考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。

// 块级作用域内部的函数声明语句,建议不要使用
{
let a = 'secret';
function f() {
return a;
}
} // 块级作用域内部,优先使用函数表达式
{
let a = 'secret';
let f = function () {
return a;
};
}
  • 有一个需要注意的地方。ES6 的块级作用域必须有大括号
// 第一种写法,报错
if (true) let x = 1; // 第二种写法,不报错
if (true) {
let x = 1;
}

const

基本用法

  • const声明一个只读的常量。一旦声明,常量的值就不能改变。

    const PI = 3.1415;
    PI // 3.1415 PI = 3;// TypeError: Assignment to constant variable.
  • const一旦声明变量,就必须立即初始化,不能留到以后赋值。

    const foo;
    // SyntaxError: Missing initializer in const declaration
  • let命令相同:只在声明所在的块级作用域内有效。

    if (true) {
    const MAX = 5;
    }
    MAX // Uncaught ReferenceError: MAX is not defined
  • 声明的常量不提升,存在暂时性死区,只能在声明的位置后面使用。

    if (true) {
    console.log(MAX); // ReferenceError
    const MAX = 5;
    }
  • const声明的常量,也与let一样不可重复声明。

    var message = "Hello!";
    let age = 25;
    // 以下两行都会报错
    const message = "Goodbye!";
    const age = 30;

本质

const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。

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']; // 报错

上面代码中,常量a是一个数组,这个数组本身是可写的,但是如果将另一个数组赋值给a,就会报错。

如果真的想将对象冻结,应该使用Object.freeze方法。

const foo = Object.freeze({});

// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};

上面是一个将对象彻底冻结的函数。

顶层对象

顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。

window.a = 1;
a // 1 a = 2;

在ES6 中,为了改变这一点,一方面为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。

var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1 let b = 1;
window.b // undefined

解构赋值

数组的解构

基本用法

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

以前,为变量赋值,只能直接指定值。

let a = 1;
let b = 2;
let c = 3;
  • ES6 允许写成下面这样。从数组中提取值,按照对应位置,对变量赋值。
let [a, b, c] = [1, 2, 3];

下面是对嵌套数组进行解构的例子

let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3 let [ , , third] = ["foo", "bar", "baz"];
third // "baz" let [x, , y] = [1, 2, 3];
x // 1
y // 3 let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4] let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []
  • 如果解构不成功,变量的值就等于undefined
let [foo] = [];
let [bar, foo] = [1];
// foo的值都等于undefined
  • 另一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组
let [x, y] = [1, 2, 3];
x // 1
y // 2 let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4
  • 如果等号的右边不是数组(或者严格地说,不是可遍历的结构,参见《Iterator》一章),那么将会报错。
// 报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
  • 对于 Set 结构,也可以使用数组的解构赋值。
let [x, y, z] = new Set(['a', 'b', 'c']);
x // "a"

默认值

  • 解构赋值允许指定默认值。
let [foo = true] = [];
foo // true let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
  • 只有当一个数组成员严格等于undefined,默认值才会生效。因为null不严格等于undefined
let [x = 1] = [undefined];
x // 1 let [x = 1] = [null];
x // null
  • 如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。
function f() {
console.log('aaa');
} let [x = f()] = [1];
//因为x能取到值,所以函数f根本不会执行。
  • 默认值可以引用解构赋值的其他变量,但该变量必须已经声明。
let [x = 1, y = x] = [];     // x=1; y=1
let [x = 1, y = x] = [2]; // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = []; // ReferenceError: y is not defined

上面最后一个表达式之所以会报错,是因为xy做默认值时,y还没有声明。

对象的解构

基本用法

解构不仅可以用于数组,还可以用于对象。

  • 对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb" let { baz } = { foo: 'aaa', bar: 'bbb' };
baz // undefined
  • 解构不成功,变量的值就等于undefined

  • 对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量。

// 例一
let { log, sin, cos } = Math; // 例二
const { log } = console;
log('hello') // hello
  • 如果变量名与属性名不一致,必须写成下面这样。
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa" let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'

这实际上说明,对象的解构赋值是下面形式的简写

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

也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。

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

上面代码中,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo

  • 解构也可以用于嵌套结构的对象。
let obj = {
p: [
'Hello',
{ y: 'World' }
]
}; let { p: [x, { y }] } = obj;
x // "Hello"
y // "World"

注意,这时p是模式,不是变量,因此不会被赋值。如果p也要作为变量赋值,可以写成下面这样。

let obj = {
p: [
'Hello',
{ y: 'World' }
]
}; let { p, p: [x, { y }] } = obj;
x // "Hello"
y // "World"
p // ["Hello", {y: "World"}]
  • 如果解构模式是嵌套的对象,而且子对象所在的父属性不存在,那么将会报错。
// 报错
let {foo: {bar}} = {baz: 'baz'};

foo这时等于undefined,再取子属性就会报错。

默认值

对象的解构也可以指定默认值。

var {x = 3} = {};
x // 3 var {x, y = 5} = {x: 1};
x // 1
y // 5 var {x: y = 3} = {};
y // 3 var {x: y = 3} = {x: 5};
y // 5 var { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"
  • 默认值生效的条件是,对象的属性值严格等于undefined。同数组一样。

注意点

  1. 如果要将一个已经声明的变量用于解构赋值,必须非常小心。
// 错误的写法
let x;
{x} = {x: 1};
// SyntaxError: syntax error

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

// 正确的写法
let x;
({x} = {x: 1});
  1. 解构赋值允许等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪的赋值表达式。表达式虽然毫无意义,但是语法是合法的,可以执行。
({} = [true, false]);
({} = 'abc');
({} = []);
  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"

类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。

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

数值布尔值的解构

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

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

数值和布尔值的包装对象都有toString属性,因此变量s都能取到值。

函数参数的解构

  • 函数的参数也可以使用解构赋值。
function add([x, y]){
return x + y;
} add([1, 2]); // 3

上面代码中,函数add的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量xy。对于函数内部的代码来说,它们能感受到的参数就是xy

例二:

[[1, 2], [3, 4]].map(([a, b]) => a + b);

  • 函数参数的解构也可以使用默认值(必须在前面)
function move({x = 0, y = 0} = {}) {
return [x, y];
} move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]

注意,下面的写法会得到不一样的结果。

function move({x, y} = { x: 0, y: 0 }) {
return [x, y];
} move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]

上面代码是为函数move的参数指定默认值,而不是为变量xy指定默认值,所以会得到与前一种写法不同的结果。

undefined就会触发函数参数的默认值。

[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]

圆括号问题

解构赋值虽然很方便,但是解析起来并不容易。对于编译器来说,一个式子到底是模式,还是表达式,没有办法从一开始就知道,必须解析到(或解析不到)等号才能知道。

不能使用圆括号的情况

  1. 变量声明语句
// 全部报错
let [(a)] = [1]; let {x: (c)} = {};
let ({x: c}) = {};
let {(x: c)} = {};
let {(x): c} = {}; let { o: ({ p: p }) } = { o: { p: 2 } };

上面6 个语句都会报错,因为它们都是变量声明语句,模式不能使用圆括号。

  1. 函数参数
// 报错
function f([(z)]) { return z; }
// 报错
function f([z,(x)]) { return x; }

函数参数也属于变量声明,因此不能带有圆括号。

  1. 赋值语句的模式
// 全部报错
({ p: a }) = { p: 42 };
([a]) = [5];

上面代码将整个模式放在圆括号之中,导致报错。

// 报错
[({ p: a }), { x: c }] = [{}, {}];

上面代码将一部分模式放在圆括号之中,导致报错。

可以使用圆括号的情况

可以使用圆括号的情况只有一种:赋值语句的非模式部分即需要赋值的变量,可以使用圆括号。

[(b)] = [3]; // 正确
({ p: (d) } = {}); // 正确
[(parseInt.prop)] = [3]; // 正确

用途

  1. 交换变量的值
let x = 1;
let y = 2; [x, y] = [y, x];
  1. 从函数返回多个值

    函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。

// 返回一个数组

function example() {
return [1, 2, 3];
}
let [a, b, c] = example(); // 返回一个对象 function example() {
return {
foo: 1,
bar: 2
};
}
let { foo, bar } = example();
  1. 函数参数的定义

    解构赋值可以方便地将一组参数与变量名对应起来

// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]); // 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
  1. 提取 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]
  1. 函数参数的默认值
jQuery.ajax = function (url, {
async = true,
beforeSend = function () {},
cache = true,
complete = function () {},
crossDomain = false,
global = true,
// ... more config
} = {}) {
// ... do stuff
};
  1. 遍历 Map 结构
const map = new Map();
map.set('first', 'hello');
map.set('second', 'world'); for (let [key, value] of map) {
console.log(key + " is " + value);
}
// first is hello
// second is world

如果只想获取键名,或者只想获取键值,可以写成下面这样。

// 获取键名
for (let [key] of map) {
// ...
} // 获取键值
for (let [,value] of map) {
// ...
}
  1. 输入模块的指定方法
const { SourceMapConsumer, SourceNode } = require("source-map");

for、for in、for of、foreach的用法和区别

一、普通for循环在Array和Object中都可以使用。

JS&ES6学习笔记(持续更新)

二、for in在Array和Object中都可以使用。

注意:在对象中包含原型上的属性。

JS&ES6学习笔记(持续更新)

三、for of在Array、Object、Set、Map中都可以使用。

JS&ES6学习笔记(持续更新)

JS&ES6学习笔记(持续更新)

四、forEach循环在Array、Set、Map中都可以使用。

JS&ES6学习笔记(持续更新)

遍历对象

Object.keys(o)

返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性).

var obj = {'0':'a','1':'b','2':'c'};

Object.keys(obj).forEach(function(key){

     console.log(key,obj[key]);

});

Object.getOwnPropertyNames(o)

返回一个数组,包含对象自身的所有属性(不含Symbol属性,但是包括不可枚举属性).

var obj = {'0':'a','1':'b','2':'c'};
Object.getOwnPropertyNames(obj).forEach(function(key){ console.log(key,obj[key]); });

Reflect.ownKeys(o)

返回一个数组,包含对象自身的所有属性,不管属性名是Symbol或字符串,也不管是否可枚举.

var obj = {'0':'a','1':'b','2':'c'};
Reflect.ownKeys(obj).forEach(function(key){ console.log(key,obj[key]); });

字符串的扩展

字符的unicode表示法

ES6 加强了对 Unicode 的支持,允许采用\uxxxx形式表示一个字符,其中xxxx表示字符的 Unicode 码点。

"\u0061"
// "a"

这种表示法只限于码点在\u0000~\uFFFF之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。

"\uD842\uDFB7"
// "
上一篇:初识 Kafka Producer 生产者


下一篇:ES6学习笔记<四> default、rest、Multi-line Strings