在線運行JS工具
如果沒有本地運行JavaScript的環境,可以使用在線的JavaScript運行工具。這裏有一個鏈接:https://jsbin.com/?js,output。
React開發中常用的JavaScript命令
在React的官方文檔中,對於React的開發之前的JavaScript背景知識提供瞭如下的幾個方面:
如果你想回顧一下 JavaScript,你可以閱讀這篇教程。注意,我們也用到了一些 ES6(較新的 JavaScript 版本)的特性。在這篇教程裏,我們主要使用了箭頭函數(arrow functions)、class、let 語句和 const 語句。你可以使用 Babel REPL 在線預覽 ES6 的編譯結果。
下面對上述JavaScript的常用命令進行說明,主要包含如下命令:
- let和const命令
- ES6 變量的解構賦值
- 函數參數的默認值
- rest參數
- 擴展運算符
- 箭頭函數
- ES6中的類class
資料參考:http://caibaojian.com/es6/object.html 和
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript
let和const命令
let命令
基本用法:ES6新增了let
命令,用來聲明變量。它的用法類似於var
,但是所聲明的變量,只在let
命令所在的代碼塊內有效。
{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1
上面代碼在代碼塊之中,分別用let
和var
聲明瞭兩個變量。然後在代碼塊之外調用這兩個變量,結果let
聲明的變量報錯,var
聲明的變量返回了正確的值。這表明,let
聲明的變量只在它所在的代碼塊有效。
for
循環的計數器,就很合適使用let
命令。
for (let i = 0; i < 10; i++) {}
console.log(i);
//ReferenceError: i is not defined
上面代碼中,計數器i
只在for
循環體內有效,在循環體外引用就會報錯。
const命令
const
聲明一個只讀的常量。一旦聲明,常量的值就不能改變。
const PI = 3.1415;
PI // 3.1415
PI = 3;
// TypeError: Assignment to constant variable.
上面代碼表明改變常量的值會報錯。const
聲明的變量不得改變值,這意味着,const一旦聲明變量,就必須立即初始化,不能留到以後賦值。
const foo;
// SyntaxError: Missing initializer in const declaration
上面代碼表示,對於const
來說,只聲明不賦值,就會報錯。const
的作用域與let
命令相同:只在聲明所在的塊級作用域內有效。
if (true) {
const MAX = 5;
}
MAX // Uncaught ReferenceError: MAX is not defined
const
命令聲明的常量也是不提升,同樣存在暫時性死區,只能在聲明的位置後面使用。
if (true) {
console.log(MAX); // ReferenceError
const MAX = 5;
}
上面代碼在常量MAX
聲明之前就調用,結果報錯。const
聲明的常量,也與let
一樣不可重複聲明。
var message = "Hello!";
let age = 25;
// 以下兩行都會報錯
const message = "Goodbye!";
const age = 30;
對於複合類型的變量,變量名不指向數據,而是指向數據所在的地址。const
命令只是保證變量名指向的地址不變,並不保證該地址的數據不變,所以將一個對象聲明爲常量必須非常小心。
ES6 變量的解構賦值
數組的解構賦值
本質上,這種寫法屬於“模式匹配”,只要等號兩邊的模式相同,左邊的變量就會被賦予對應的值。下面是一些使用嵌套數組進行解構的例子。
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
。
var [foo] = [];
var [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
上面兩個例子,都屬於不完全解構,但是可以成功。
對象的解構賦值
解構不僅可以用於數組,還可以用於對象。
var { foo, bar } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
對象的解構與數組有一個重要的不同。數組的元素是按次序排列的,變量的取值由它的位置決定;而對象的屬性沒有次序,變量必須與屬性同名,才能取到正確的值。
var { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
var { baz } = { foo: "aaa", bar: "bbb" };
baz // undefined
第一個例子,等號左邊的兩個變量的次序,與等號右邊兩個同名屬性的次序不一致,但是對取值完全沒有影響。第二個例子的變量沒有對應的同名屬性,導致取不到值,最後等於undefined
。
這實際上說明,對象的解構賦值是下面形式的簡寫(參見《對象的擴展》一章)。
var { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };
也就是說,對象的解構賦值的內部機制,是先找到同名屬性,然後再賦給對應的變量。真正被賦值的是後者,而不是前者。
var { foo: baz } = { foo: "aaa", bar: "bbb" };
baz // "aaa"
foo // error: foo is not defined
上面代碼中,foo
是匹配的模式,baz
纔是變量。真正被賦值的是變量baz
,而不是模式foo
。
函數參數的解構賦值
函數的參數也可以使用解構賦值。
function add([x, y]){
return x + y;
}
add([1, 2]); // 3
上面代碼中,函數add
的參數表面上是一個數組,但在傳入參數的那一刻,數組參數就被解構成變量x
和y
。對於函數內部的代碼來說,它們能感受到的參數就是x
和y
。
下面是另一個例子。
[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [ 3, 7 ]
函數參數的解構也可以使用默認值。
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]
上面代碼中,函數move
的參數是一個對象,通過對這個對象進行解構,得到變量x
和y
的值。如果解構失敗,x
和y
等於默認值。
注意,下面的寫法會得到不一樣的結果。
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
的參數指定默認值,而不是爲變量x
和y
指定默認值,所以會得到與前一種寫法不同的結果。
undefined
就會觸發函數參數的默認值。
[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]
函數參數的默認值
基本用法
ES6允許爲函數的參數設置默認值,即直接寫在參數定義的後面。
function log(x, y = 'World') {
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
可以看到,ES6的寫法比ES5簡潔許多,而且非常自然。下面是另一個例子。
function Point(x = 0, y = 0) {
this.x = x;
this.y = y;
}
var p = new Point();
p // { x: 0, y: 0 }
參數變量是默認聲明的,所以不能用let
或const
再次聲明。
function foo(x = 5) {
let x = 1; // error
const x = 2; // error
}
上面代碼中,參數變量x
是默認聲明的,在函數體中,不能用let
或const
再次聲明,否則會報錯。
與解構賦值默認值結合使用
參數默認值可以與解構賦值的默認值,結合起來使用。
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() // TypeError: Cannot read property 'x' of undefined
上面代碼使用了對象的解構賦值默認值,而沒有使用函數參數的默認值。只有當函數foo
的參數是一個對象時,變量x
和y
纔會通過解構賦值而生成。如果函數foo
調用時參數不是對象,變量x
和y
就不會生成,從而報錯。如果參數對象沒有y
屬性,y
的默認值5纔會生效。
下面是另一個對象的解構賦值默認值的例子。
function fetch(url, { body = '', method = 'GET', headers = {} }) {
console.log(method);
}
fetch('http://example.com', {})
// "GET"
fetch('http://example.com')
// 報錯
上面代碼中,如果函數fetch
的第二個參數是一個對象,就可以爲它的三個屬性設置默認值。
上面的寫法不能省略第二個參數,如果結合函數參數的默認值,就可以省略第二個參數。這時,就出現了雙重默認值。
function fetch(url, { method = 'GET' } = {}) {
console.log(method);
}
fetch('http://example.com')
// "GET"
上面代碼中,函數fetch
沒有第二個參數時,函數參數的默認值就會生效,然後纔是解構賦值的默認值生效,變量method
纔會取到默認值GET
。
再請問下面兩種寫法有什麼差別?
// 寫法一
function m1({x = 0, y = 0} = {}) {
return [x, y];
}
// 寫法二
function m2({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
上面兩種寫法都對函數的參數設定了默認值,區別是寫法一函數參數的默認值是空對象,但是設置了對象解構賦值的默認值;寫法二函數參數的默認值是一個有具體屬性的對象,但是沒有設置對象解構賦值的默認值。
// 函數沒有參數的情況
m1() // [0, 0]
m2() // [0, 0]
// x和y都有值的情況
m1({x: 3, y: 8}) // [3, 8]
m2({x: 3, y: 8}) // [3, 8]
// x有值,y無值的情況
m1({x: 3}) // [3, 0]
m2({x: 3}) // [3, undefined]
// x和y都無值的情況
m1({}) // [0, 0];
m2({}) // [undefined, undefined]
m1({z: 3}) // [0, 0]
m2({z: 3}) // [undefined, undefined]
參數默認值的位置
通常情況下,定義了默認值的參數,應該是函數的尾參數。因爲這樣比較容易看出來,到底省略了哪些參數。如果非尾部的參數設置默認值,實際上這個參數是沒法省略的。
// 例一
function f(x = 1, y) {
return [x, y];
}
f() // [1, undefined]
f(2) // [2, undefined])
f(, 1) // 報錯
f(undefined, 1) // [1, 1]
// 例二
function f(x, y = 5, z) {
return [x, y, z];
}
f() // [undefined, 5, undefined]
f(1) // [1, 5, undefined]
f(1, ,2) // 報錯
f(1, undefined, 2) // [1, 5, 2]
上面代碼中,有默認值的參數都不是尾參數。這時,無法只省略該參數,而不省略它後面的參數,除非顯式輸入undefined
。
如果傳入undefined
,將觸發該參數等於默認值,null
則沒有這個效果。
function foo(x = 5, y = 6) {
console.log(x, y);
}
foo(undefined, null)
// 5 null
上面代碼中,x
參數對應undefined
,結果觸發了默認值,y
參數等於null
,就沒有觸發默認值。
rest參數
ES6引入rest參數(形式爲“…變量名”),用於獲取函數的多餘參數,這樣就不需要使用arguments對象了。rest參數搭配的變量是一個數組,該變量將多餘的參數放入數組中。
function add(...values) { // 這裏的values實際上是一個數組
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10
上面代碼的add函數是一個求和函數,利用rest參數,可以向該函數傳入任意數目的參數。
下面是一個rest參數代替arguments變量的例子。
// arguments變量的寫法
function sortNumbers() {
return Array.prototype.slice.call(arguments).sort();
}
// rest參數的寫法
const sortNumbers = (...numbers) => numbers.sort();
上面代碼的兩種寫法,比較後可以發現,rest參數的寫法更自然也更簡潔。
rest參數中的變量代表一個數組,所以數組特有的方法都可以用於這個變量。下面是一個利用rest參數改寫數組push方法的例子。
function push(array, ...items) {
items.forEach(function(item) {
array.push(item);
console.log(item);
});
}
var a = [];
push(a, 1, 2, 3)
注意,rest參數之後不能再有其他參數(即只能是最後一個參數),否則會報錯。
// 報錯
function f(a, ...b, c) {
// ...
}
函數的length屬性,不包括rest參數。
(function(a) {}).length // 1
(function(...a) {}).length // 0
(function(a, ...b) {}).length // 1
擴展運算符
含義:擴展運算符(spread)是三個點(...
)。它好比rest參數的逆運算,將一個數組轉爲用逗號分隔的參數序列。
console.log(...[1, 2, 3])
// 1 2 3
console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5
[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]
該運算符主要用於函數調用。
function push(array, ...items) {
array.push(...items);
}
function add(x, y) {
return x + y;
}
var numbers = [4, 38];
add(...numbers) // 42
上面代碼中,array.push(...items)
和add(...numbers)
這兩行,都是函數的調用,它們的都使用了擴展運算符。該運算符將一個數組,變爲參數序列。
擴展運算符與正常的函數參數可以結合使用,非常靈活。
function f(v, w, x, y, z) { }
var args = [0, 1];
f(-1, ...args, 2, ...[3]);
箭頭函數
引入箭頭函數有兩個方面的作用:更簡短的函數並且不綁定this。
基本語法:
(param1, param2, …, paramN) => { statements }
(param1, param2, …, paramN) => expression
//相當於:(param1, param2, …, paramN) =>{ return expression; }
// 當只有一個參數時,圓括號是可選的:
(singleParam) => { statements }
singleParam => { statements }
// 沒有參數的函數應該寫成一對圓括號。
() => { statements }
高級語法:
//加括號的函數體返回對象字面量表達式:
params => ({foo: bar})
//支持剩餘參數和默認參數
(param1, param2, ...rest) => { statements }
(param1 = defaultValue1, param2, …, paramN = defaultValueN) => {
statements }
//同樣支持參數列表解構
let f = ([a, b] = [1, 2], {x: c} = {x: a + b}) => a + b + c;
f(); // 6
ES6允許使用“箭頭”(=>
)定義函數。
var f = v => v;
上面的箭頭函數等同於:
var f = function(v) {
return v;
};
如果箭頭函數不需要參數或需要多個參數,就使用一個圓括號代表參數部分。
var f = () => 5;
// 等同於
var f = function () { return 5 };
var sum = (num1, num2) => num1 + num2;
// 等同於
var sum = function(num1, num2) {
return num1 + num2;
};
如果箭頭函數的代碼塊部分多於一條語句,就要使用大括號將它們括起來,並且使用return
語句返回。
var sum = (num1, num2) => { return num1 + num2; }
由於大括號被解釋爲代碼塊,所以如果箭頭函數直接返回一個對象,必須在對象外面加上括號。
var getTempItem = id => ({ id: id, name: "Temp" });
箭頭函數可以與變量解構結合使用。
const full = ({ first, last }) => first + ' ' + last;
// 等同於
function full(person) {
return person.first + ' ' + person.last;
}
箭頭函數使得表達更加簡潔。
const isEven = n => n % 2 == 0;
const square = n => n * n;
上面代碼只用了兩行,就定義了兩個簡單的工具函數。如果不用箭頭函數,可能就要佔用多行,而且還不如現在這樣寫醒目。
箭頭函數的一個用處是簡化回調函數。
// 正常函數寫法
[1,2,3].map(function (x) {
return x * x;
});
// 箭頭函數寫法
[1,2,3].map(x => x * x);
另一個例子是
// 正常函數寫法
var result = values.sort(function (a, b) {
return a - b;
});
// 箭頭函數寫法
var result = values.sort((a, b) => a - b);
下面是rest參數與箭頭函數結合的例子。
const numbers = (...nums) => nums;
numbers(1, 2, 3, 4, 5)
// [1,2,3,4,5]
const headAndTail = (head, ...tail) => [head, tail];
headAndTail(1, 2, 3, 4, 5)
// [1,[2,3,4,5]]
使用注意點
箭頭函數有幾個使用注意點。
(1)函數體內的this
對象,就是定義時所在的對象,而不是使用時所在的對象。
(2)不可以當作構造函數,也就是說,不可以使用new
命令,否則會拋出一個錯誤。
(3)不可以使用arguments
對象,該對象在函數體內不存在。如果要用,可以用Rest參數代替。
(4)不可以使用yield
命令,因此箭頭函數不能用作Generator函數。
上面四點中,第一點尤其值得注意。this
對象的指向是可變的,但是在箭頭函數中,它是固定的。
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
var id = 21;
foo.call({ id: 42 });
// id: 42
上面代碼中,setTimeout
的參數是一個箭頭函數,這個箭頭函數的定義生效是在foo
函數生成時,而它的真正執行要等到100毫秒後。如果是普通函數,執行時this
應該指向全局對象window
,這時應該輸出21
。但是,箭頭函數導致this
總是指向函數定義生效時所在的對象(本例是{id: 42}
),所以輸出的是42
。
ES6中的類class
ES6提供了更接近傳統語言的寫法,引入了Class(類)這個概念,作爲對象的模板。通過class
關鍵字,可以定義類。基本上,ES6的class
可以看作只是一個語法糖,它的絕大部分功能,ES5都可以做到,新的class
寫法只是讓對象原型的寫法更加清晰、更像面向對象編程的語法而已。上面的代碼用ES6的“類”改寫,就是下面這樣。
//定義類
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
上面代碼定義了一個“類”,可以看到裏面有一個constructor
方法,這就是構造方法,而this
關鍵字則代表實例對象。也就是說,ES5的構造函數Point
,對應ES6的Point
類的構造方法。
Point
類除了構造方法,還定義了一個toString
方法。注意,定義“類”的方法的時候,前面不需要加上function
這個關鍵字,直接把函數定義放進去了就可以了。另外,方法之間不需要逗號分隔,加了會報錯。ES6的類,完全可以看作構造函數的另一種寫法。
class Point {
// ...
}
typeof Point // "function"
Point === Point.prototype.constructor // true
上面代碼表明,類的數據類型就是函數,類本身就指向構造函數。
使用的時候,也是直接對類使用new
命令,跟構造函數的用法完全一致。
class Bar {
doStuff() {
console.log('stuff');
}
}
var b = new Bar();
b.doStuff() // "stuff"
構造函數的prototype
屬性,在ES6的“類”上面繼續存在。事實上,類的所有方法都定義在類的prototype
屬性上面。
class Point {
constructor(){
// ...
}
toString(){
// ...
}
toValue(){
// ...
}
}
// 等同於
Point.prototype = {
toString(){},
toValue(){}
};
在類的實例上面調用方法,其實就是調用原型上的方法。
class B {}
let b = new B();
b.constructor === B.prototype.constructor // true
上面代碼中,b
是B類的實例,它的constructor
方法就是B類原型的constructor
方法。
類的實例對象
生成類的實例對象的寫法,與ES5完全一樣,也是使用new
命令。如果忘記加上new
,像函數那樣調用Class
,將會報錯。
// 報錯
var point = Point(2, 3);
// 正確
var point = new Point(2, 3);
與ES5一樣,實例的屬性除非顯式定義在其本身(即定義在this
對象上),否則都是定義在原型上(即定義在class
上)。
//定義類
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
var point = new Point(2, 3);
point.toString() // (2, 3)
point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true
上面代碼中,x
和y
都是實例對象point
自身的屬性(因爲定義在this
變量上),所以hasOwnProperty
方法返回true
,而toString
是原型對象的屬性(因爲定義在Point
類上),所以hasOwnProperty
方法返回false
。這些都與ES5的行爲保持一致。
Class表達式
與函數一樣,類也可以使用表達式的形式定義。
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
上面代碼使用表達式定義了一個類。需要注意的是,這個類的名字是MyClass
而不是Me
,Me
只在Class的內部代碼可用,指代當前類。
let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined
上面代碼表示,Me
只在Class內部有定義。
如果類的內部沒用到的話,可以省略Me
,也就是可以寫成下面的形式。
const MyClass = class { /* ... */ };
採用Class表達式,可以寫出立即執行的Class。
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}('張三');
person.sayName(); // "張三"
上面代碼中,person
是一個立即執行的類的實例。
this的指向
類的方法內部如果含有this
,它默認指向類的實例。但是,必須非常小心,一旦單獨使用該方法,很可能報錯。
class Logger {
printName(name = 'there') {
this.print(`Hello ${name}`);
}
print(text) {
console.log(text);
}
}
const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined
上面代碼中,printName
方法中的this
,默認指向Logger
類的實例。但是,如果將這個方法提取出來單獨使用,this
會指向該方法運行時所在的環境,因爲找不到print
方法而導致報錯。
一個比較簡單的解決方法是,在構造方法中綁定this
,這樣就不會找不到print
方法了。
class Logger {
constructor() {
this.printName = this.printName.bind(this);
}
// ...
}
另一種解決方法是使用箭頭函數(推薦使用)。
class Logger {
constructor() {
this.printName = (name = 'there') => {
this.print(`Hello ${name}`);
};
}
// ...
}
還有一種解決方法是使用Proxy
,獲取方法時自動綁定this
。
function selfish (target) {
const cache = new WeakMap();
const handler = {
get (target, key) {
const value = Reflect.get(target, key);
if (typeof value !== 'function') {
return value;
}
if (!cache.has(value)) {
cache.set(value, value.bind(target));
}
return cache.get(value);
}
};
const proxy = new Proxy(target, handler);
return proxy;
}
const logger = selfish(new Logger());