前言
看书学习React && 手动实现书中代后的一周,内心对于ES6、React自身的一些特性很混淆,不知道代码中哪一些是ES6的语法和特性,哪些是React自身的语法, 哪些又是JavaScript自带的用法。
学习参考资料
给React初学者的10分钟ES6教程
深入理解ES6
ECMAScript 6 入门
JavaScript 标准参考教程 - 语法专题 - ECMAScript 6 介绍
ES6
声明变量的关键字:var、let 和 const
JavaScript中,我们通常说的作用域是函数作用域,使用var声明的变量,无论是在代码的哪个地方声明的,都会提升到当前作用域的最顶部,这种行为叫做变量提升(Hoisting)。
也就是说,如果在函数内部声明的变量,都会被提升到该函数开头,而在全局声明的变量,就会提升到全局作用域的顶部。
没有声明和声明后没有赋值是不一样
在ES5中,使用 var
去声明变量,var
命令会发生”变量提升“现象,即变量可以在声明之前使用,值为undefined。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。
//ES5
console.log(myVar); // prints out undefined
var myVar = 10;
在ES6中,使用 let
和 const
声明变量,并且必须提前声明变量才可以使用。
//ES6
console.log(myVar); // ReferenceError myVar is not defined
let myVar = 10;
在ES5 中只有全局作用域和函数作用域,没有块级作用域。而 let
则实际上为JavaScript新增了块级作用域。用let
声明的变量,只在let
命令所在的代码块内有效。
Note:
-
const
的作用域与let
命令相同:只在声明所在的块级作用域内有效。 -
const
声明的变量不得改变值,这意味着,const
一旦声明变量,就必须立即初始化,不能留到以后赋值。 -
const
命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。 - 当前使用块级绑定的最佳实践是:默认使用
const
,只在确实需要改变变量值时使用let
。
暂时性死区
只要块级作用域内存在let
命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
ES6 明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
let x = 10;
const y = 20;
x = 25; //OK
y = 30; // TypeError: Assignment to constant variable.
ES5 只有两种声明变量的方法:var
命令和function
命令。ES6 除了添加let
和const
命令,另外两种声明变量的方法:import
命令和class
命令。所以,ES6 一共有 6 种声明变量的方法。
ES6函数
ES6中的构造函数Function新增了支持默认参数和不定参数。
函数的默认参数(ES6 语法糖)
ES5 中,我们给函数传参,然后在函数体内设置默认值。
function a(num, callback) {
num = num || 6;
callback = callback || function (data) { console.log('ES5:', data) };
callback(num * num);
}
a() //ES5: 36
a(10, function (data) {
console.log(data * 10); //1000, 传参输出新数值
})
ES6中,我们可以使用新的默认值写法。这种写法可以让函数体内部的代码更加简洁优雅。
function b(num = 6, callback = function (data) { console.log('ES6:', data) }) {
callback(num * num);
}
b() //ES6: 36
b(10, function (data) { console.log(data * 10) }); //1000, 传参输出新数值
修改默认参数对arguments的影响
arguments对象是什么?
准确一点来说它是一个类数组对象,它存在函数内部,它将当前函数的所有参数组成了一个类数组对象。
function e(x, y) {
console.log(arguments); //[Arguments] { '0': 1, '1': 2 }
console.log(arguments.length); //2
}
e(1,2)
当函数带有默认值的时候,arguments对象是读取不到默认参数的值。
function f(x = 1, y = 2) {
console.log(arguments);
}
f() //[Arguments] {}
f(4, 5) //[Arguments] { '0': 4, '1': 5 }
在ES5的非严格模式下,一开始输入的参数是1,那么可以获取到arguments[0] (表示第一个参数)全等于num,修改num=2后,arguments[0] 也能更新到2。
function c(num){
console.log(num === arguments[0]) //true
num = 2 //修改参数默认值
console.log(num === arguments[0]) //true
}
c(1);
在ES5的严格模式,这个不知道为什么在create-react-app 创建的工程下不生效,都是true
"use strict"; //严格模式
function d(num) {
console.log(num === arguments[0]); // true
num = 2;
console.log(num === arguments[0]); // true????
}
d(1);
在ES6环境下,默认值对arguments的影响和ES5严格模式是同样的标准。
默认参数表达式
参数不仅可以设置默认值为字符串,数字,数组或者对象,还可以是一个函数。
const add = () => 10;
function g(num = add()) {
console.log(num);
}
g(); //10
无命名参数
当传入的参数是一个对象,不是一个具体的参数名,则是无命名参数。
const demo = (object) => console.log(object.a + object.b);
let obj = {
a: 1,
b: 2
}
demo(obj);
不定参数
不定参数使用...(展开运算符)的参数就是不定参数,它表示一个数组.
限制:...参数必须放在所有的参数末尾,不能用于对象的setter方法中。
const add_num = (...arr) => {
console.log(a + b);
}
let a = 1, b = 2;
add_num(a, b) //3
// 使用带有不定参数的函数时,参数的声明是要和函数体中使用的具体参数名称一致
// 错误的写法
let obj = {
set add(...arr){}
}
箭头函数(ES6 语法糖)
箭头函数的声明
// ES5 声明函数的写法
var sum = function(a, b) {
return a + b;
}
ES6 箭头函数声明注意事项:
- 如果箭头函数不需要参数 或者 需要多个参数,就要使用圆括号代表参数部分
- 如果箭头函数有且仅有一个参数,可以省略圆括号
- 如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return 语句返回
- 由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号。
// ES6 函数写法
// 如果箭头函数不需要参数 或者 需要多个参数,就要使用圆括号代表参数部分
const f = () => 5;
const sum = (a, b) => { return a + b }
// 如果箭头函数有且仅有一个参数,可以省略圆括号
const print = name => console.log(name);
// 如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return 语句返回。
const sum2 = (num1, num2) => { return num1 + num2; }
//由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号。
var getTempItem = id => ({ id: id, name: "Temp" });
箭头函数和普通函数的区别:
- 箭头函数没有 this, 函数内部的this 来自于父级最近的非箭头函数, 并且不能改变 this 的指向。(还有一种描述:函数体内的this对象,绑定定义时所在的对象,而不是使用时所在的对象。
) - 箭头函数没有 super
- 箭头函数没有 arguments
- 箭头函数没有 new.targert 绑定
- 不可以当作构造函数,也就是说,不可以使用 new命令,会报错
- 没有原型
- 不支持重复的命名
对象
对象字面量语法扩展
在ES6 中,可以用更少的代码表示对象的键值对。
属性初始值简写
//ES5
function a(id) {
return {
id: id
};
}
// ES6
const a6 = (id) => ({id})
str = "Hello"
number = 20
//ES5
obj = {
str: str,
number: number
}
// ES6
obj6 = {str, number}
对象方法简写
// ES5
const obj = {
id: 1,
printId: function () {
console.log(this.id)
}
}
// ES6
const obj = {
id: 1,
printId() {
console.log(this.id)
}
}
解构 (Destructuring)
ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构。
解构分类:
- 数组解构
- 对象解构
- 字符串解构
数组解构赋值
从数组中提取值,然后按照对应的位置,对变量赋值。
//ES6
let [a, b, c] = [1, 2, 3];
本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。下面是一些使用嵌套数组进行解构的例子。
如果解构不成功,变量的值就等于undefined。
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 // []
let [foo] = []; // foo undefined
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
如果等号的右边不是数组(或者严格地说,不是可遍历的结构),那么将会报错。
// 报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
解构赋值允许指定默认值。
let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
注意,ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined,默认值才会生效。
let [x = 1] = [undefined];
x // 1
let [x = 1] = [null];
x // null
对象的解构赋值
对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
let { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
let { baz } = { foo: "aaa", bar: "bbb" };
baz // undefined
如果变量名与属性名不一致,必须写成下面这样。
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
let obj = { first: 'hello', last: 'world' };
let { first: x, last: y } = obj;
x // 'hello'
y // '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。
对象的解构也可以指定默认值。
var {x = 3} = {};
x // 3
var {x, y = 5} = {x: 1};
x // 1
y // 5
函数参数的解构赋值
解构是将对象或者数组中的元素一个个提取出来,而赋值是给元素赋值,解构赋值的作用就是给对象或者数组的元素赋值。
在react的父子组件传递参数过程中,就使用到了解构赋值。
class Parent extends React.Component {
render() {
const {a = 3, b = 3} = this.props
return {a}-{b}
}
}
ReactDOM.render(
,
document.getElementById('root')
);
函数参数默认值与解构赋值默认值结合使用
// 这个只使用了解构赋值默认值,没有使用函数参数默认值
function foo ({x, y=5}){
console.log(x, y);
}
foo({}); // undefined 5
foo({x: 1}) // 1 5
foo({x: 1, y: 2}); //1 2
foo() // Cannot destructure property `x` of 'undefined' or 'null'.
// 使用了参数默认值,然后是解构赋值默认值
// 如果没有提供参数,函数foo的参数默认为一个空对象。
function foo ({x, y=5} = {}){
console.log(x, y);
}
foo() // undefined 5
function fetch(url, { body = '', method = 'GET', headers = {} }) {
console.log(method);
}
fetch('http://example.com', {})
// "GET"
fetch('http://example.com')
// 报错
上面代码中,如果函数fetch的第二个参数是一个对象,就可以为它的三个属性设置默认值。这种写法不能省略第二个参数,如果结合函数参数的默认值,就可以省略第二个参数。这时,就出现了双重默认值。
function fetch(url, { body = '', method = 'GET', headers = {} } = {}) {
console.log(method);
}
fetch('http://example.com')
// "GET"
字符串的解构赋值
字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。
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 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();
- 函数参数的定义 和 数参数的默认值
解构赋值可以方便地将一组参数与变量名对应起来。
// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);
// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
function f([x=100, y, z]) {
console.log(x)
console.log(y)
console.log(z)
}
f([ , 2, 3]); // 100 2 3
- 遍历 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);
}
// first is hello
// second is world
- 输入模块的指定方法
加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。
import { SourceMapConsumer, SourceNode } from './xxx.js'
模块的定义
模块是自动运行在严格模式下并且没有办法退出运行的JavaScript代码。
模块可以是函数、数据、类,需要指定导出的模块名,才能被其他模块访问。
//数据模块
const obj = { a: 1 }
//函数模块
const sum = (a, b) => {
return a + b
}
//类模块
class My extends React.Components {
}
模块的导出
给数据、函数、类添加一个export,就能导出模块。一个配置型的JavaScript文件中,你可能会封装多种函数,然后给每个函数加上一个export关键字,就能在其他文件访问到。
//数据模块
export const obj = {a: 1}
//函数模块
export const sum = (a, b) => {
return a + b
}
//类模块
export class My extends React.Components {
}
模块的引用
在另外的js文件中,我们可以引用上面定义的模块。使用import关键字,导入分2种情况,一种是导入指定的模块,另外一种是导入全部模块。
导入指定的模块
//导入obj数据,My类
import {obj, My} from './xx.js'
//使用
console.log(obj, My)
导入全部模块
//导入全部模块
import * as all from './xx.js'
//使用
console.log(all.obj, all.sun(1, 2), all.My)
默认模块的使用
如果给我们的模块加上default关键字,那么该js文件默认只导出该模块,你还需要把大括号去掉。
//默认模块的定义
function sum(a, b) {
return a + b
}
export default sum
//导入默认模块
import sum from './xx.js'
模块的使用限制
不能在语句和函数之内使用import关键字,只能在模块顶部使用。
修改模块导入和导出名
- 在模块导出的时候修改
function sum(a, b) {
return a + b
}
export { sum as add }
import { add } from './xx.js'
add(1, 2)
- 在模块导入的时候修改
function sum(a, b) {
return a + b
}
export sum
import { sum as add } from './xx.js'
add(1, 2)
无绑定导入
当你的模块没有可导出模块,全都是定义的全局变量的时候,你可以使用无绑定导入。
模块
let a = 1
const PI = 3.1314
无绑定导入:
import './xx.js'
console.log(a, PI)
模板字符串 (ES6 语法糖)
模板字符串(template string)是增强版的字符串,即可以当作普通字符串使用,也可以在字符串中嵌入变量。它用反引号(`)标识。
// 普通字符串
console.log(`In JavaScript '\n' is a line-feed.`);
// 多行字符串
console.log(`In JavaScript this is
not legal.`);
// 字符串中嵌入变量
var name = "Bob", time = "today";
console.log(`Hello ${name}, how are you ${time}?`);
var x = 1;
var y = 2;
console.log(`${ x } + ${ y } = ${ x + y}`)
// "1 + 2 = 3"