React開發中常用JavaScript(ES6)基礎知識

在線運行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的常用命令進行說明,主要包含如下命令:

  1. let和const命令
  2. ES6 變量的解構賦值
  3. 函數參數的默認值
  4. rest參數
  5. 擴展運算符
  6. 箭頭函數
  7. 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

上面代碼在代碼塊之中,分別用letvar聲明瞭兩個變量。然後在代碼塊之外調用這兩個變量,結果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的參數表面上是一個數組,但在傳入參數的那一刻,數組參數就被解構成變量xy。對於函數內部的代碼來說,它們能感受到的參數就是xy
下面是另一個例子。

[[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的參數是一個對象,通過對這個對象進行解構,得到變量xy的值。如果解構失敗,xy等於默認值。
注意,下面的寫法會得到不一樣的結果。

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 ]

函數參數的默認值

基本用法

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 }

參數變量是默認聲明的,所以不能用letconst再次聲明。

function foo(x = 5) {
  let x = 1; // error
  const x = 2; // error
}

上面代碼中,參數變量x是默認聲明的,在函數體中,不能用letconst再次聲明,否則會報錯

與解構賦值默認值結合使用

參數默認值可以與解構賦值的默認值,結合起來使用。

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的參數是一個對象時,變量xy纔會通過解構賦值而生成。如果函數foo調用時參數不是對象,變量xy就不會生成,從而報錯。如果參數對象沒有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

上面代碼中,xy都是實例對象point自身的屬性(因爲定義在this變量上),所以hasOwnProperty方法返回true,而toString是原型對象的屬性(因爲定義在Point類上),所以hasOwnProperty方法返回false。這些都與ES5的行爲保持一致。

Class表達式

與函數一樣,類也可以使用表達式的形式定義。

const MyClass = class Me {
  getClassName() {
    return Me.name;
  }
};

上面代碼使用表達式定義了一個類。需要注意的是,這個類的名字是MyClass而不是MeMe只在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());
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章