《JavaScript程式設計精解》--讀書筆記

赤木晴子發表於2019-02-13

第1章 值、型別和運算子

1.1 值

在JavaScript中包含6種基本的值型別:數字(number)、字串(string)、布林值(boolean)、物件(object)、函式(function)、和未定義型別(undefined)

1.2 數字

數字(number)型別的值即數字值。在JavaScript中寫成如下形式:

13

注意:在處理分數的時候,將其視為近似值,而非精確值

1.2.1 算術

JavaScript中的算術運算如下所示:

100 + 4 * 11

1.2.2 特殊數字

正無窮大: Infinity

負無窮大:-Infinity

非數值: NaN

1.3 字串

我們使用字串來表示文字資訊。使用引號將內容括起來。

"Patch my boat with chewing gum"

'Monkeys wave goodbye'

1.4 一元運算子

console.log(typeof 4.5); // number
console.log(typeof "x"); // string
複製程式碼

1.5 布林值

該型別只有兩種取值:true和false

1.5.1 比較

console.log(3 > 2); // true

console.log(3 < 2); // false

console.log("Aardark" < "Zoroaster"); // true

console.log( NaN == NaN); // false
複製程式碼

1.5.2 邏輯運算子

JavaScript支援三種邏輯運算子:與(and)、或(or)、非(not)

console.log(true && false); // false
console.log(true && true); // true

console.log(false || true); // true
console.log(false || false); // false

// 三元運算子
console.log(true ? 1 : 2); // 1
console.log(false ? 1 : 2); // 2
複製程式碼

1.6 未定義值

null和undefined用於表示無意義的值。它們各自表示其自身含義,除此之外不包含任何資訊。

1.7 自動型別轉換

console.log(8 * null); // 0
console.log("5" - 1); // 4
console.log("5" + 1); // 51
console.log("five" * 2); // NaN
console.log(false == 0); // true
console.log("" === false); // false
複製程式碼

邏輯運算子的短路特性

console.log(null || "user"); // user
console.log("Karel" || "user"); // Karel
複製程式碼

第2章 程式結構

2.1 表示式和語句

最簡單的一條語句由一個表示式和其後的分號組成。比如這就是一個程式:

1;
!false;
複製程式碼

2.2 變數

var caught = 5 * 5;
var ten = 10;
console.log(ten * ten); // 100

var mood = "light";
console.log(mood); // light
mood = "dark";
console.log(mood); // dark

var luigisDebt = 140;
luigisDebt = luigisDebt - 35;
console.log(luigisDebt); // 105

var one = 1,two = 2;
console.log(one + two); // 3
複製程式碼

2.3 關鍵字和保留字

某些具有特殊含義的單詞稱之為關鍵字,比如var,關鍵字不能作為變數名。另外還有一些單詞預留給JavaScript使用。

2.4 環境

我們將給定時間內的變數和變數值的集合稱為環境。

2.5 函式

在預設環境中包含了很多函式型別的值。函式是指包裝在變數中的一段程式,我們可以使用這些值來執行包裝好的程式。

alert("Good morning!");
複製程式碼

2.6 console.log函式

var x = 30;
console.log("the value of x is ",x); // the value of x is 30
複製程式碼

2.7 返回值

我們將函式生成值的操作稱之為返回值。

console.log(Math.max(2,4); // 4

console.log(Math.min(2,4) + 100); // 102
複製程式碼

2.8 prompt和confirm函式

在現代web程式設計當中很少使用這兩個函式,主要原因是你無法對彈出視窗的風格進行控制。但對於小程式或測試程式來說,這兩個函式還是非常有用的。

confirm("Shall we,then?");

prompt("Tell me everything you know.","...");
複製程式碼

2.9 控制流

若你的程式中包含了不止一條語句,那麼這些語句一定是按照從上到下的順序執行的。

var theNumber = Number(prompt("Pick a number","");
alert("Your number is the square root of " + theNumber * theNumber);
複製程式碼

2.10 條件執行

var theNumber = Number(prompt("Pick a number","");
if(!isNaN(theNumber)){
    alert("Your number is the square root of " + theNumber * theNumber);
}else{
    alert("Hey,Why didn't you give me a number?");
}

var num = Number(prompt("Pick a number","");
if(num < 10){
    alert("small");
}else if(num < 100){
    alert("medium");
}else{
    alert("large");
}
複製程式碼

2.11 while和do迴圈

var number = 0;
while(number <= 12){
    console.log(number);
    number = number + 2;
}

// 編寫一個程式用於顯示2的10次方
var result = 1;
var counter = 0;
while(counter < 10){
    result = result * 2;
    counter++;
}
console.log(result);

do{
    var name = prompt("who are you?");
}while(!name);
console.log(name);
複製程式碼

2.12 程式碼縮排

2.13 for迴圈

for(var number = 0; number <= 12;number = number + 2){
    console.log(number);
}

var result = 1;
for(var counter = 0;counter < 10;counter++){
    result = result * 2;
}
console.log(result);
複製程式碼

2.14 跳出迴圈

for(var current = 20; ; current++){
    if(current % 7 == 0){
        break;
    }
}
console.log(current); // 21
複製程式碼

2.15 更新變數的簡便方法

counter += 1;
result *= 2;
counter++;
counter--;
複製程式碼

2.16 switch條件分支

switch(prompt("What is the weather like?")){
    case "rainly":
    console.log("Remember to bring an umbrella");
    break;
    case "sunny":
    console.log("Dress lightly");
    break;
    case "cloudly":
    console.log("Go outside");
    break;
    default:
    console.log("Unknown weather type!");
    break;
}
複製程式碼

2.17 大寫

2.18 註釋

var accountBalance = calculateBalance(account);
// It's a green hollow where a river sings
accountBalance.adjust();
// Madly catching white tatters in the grass
var report = new Report();
// Where the sun on the proud mountain rings:
addToReport(accountBalance,report);
// It's a little valley,foaming like light in a glass

/*
I first found ths number scrawled on the back of one of
my notebooks a few years ago.Since then,it has often
dropped by,showing up in phone numbers and the serial 
numbers of products that I've bought.It obviously likes
me,so I've decided to keep it.
*/
var myNumber = 112213;
複製程式碼

2.19 本章小結

2.20 習題

// 1
var str = "";
for(var i = 0; i < 7;i++){
    str = str + "#";
    console.log(str);
}

// 2
for(var i = 1; i <= 100;i++){
    if(i % 3 == 0){
        console.log("Fizz");
    }else if(i % 5 == 0){
        console.log("Buzz")
    }else{
        console.log(i);
    }
}

for(var i = 1; i <= 100;i++){
    if(i % 3 == 0 && i % 5 == 0){
        console.log("FizzBuzz");
    }else if(i % 3 == 0){
        console.log("Fizz")
    }else if(i % 5 == 0){
        console.log("Buzz")
    }else{
        console.log(i);
    }
}

// 3
var str = "";
var size = 8;
for(var i = 0; i < size; i++){
    for(var j = 0; j < size;j++){
        if((i + j)% 2 == 0){
            str = str + "#";
        }else{
            str = str + " ";
        }
    }
 str += "\n";   
}
console.log(str);
複製程式碼

第3章 函式

3.1 定義函式

一個函式定義就是普通的變數定義,只不過變數型別恰好是函式。

建立函式的表示式以關鍵字function開頭。

某些函式可以產生值,而一些函式不會產生值。函式中的return語句決定了函式的返回值。當函式執行到return語句時,會立即跳出當前函式,並將返回值賦給呼叫者。如果return關鍵字後沒有任何表示式,則該函式返回undefined。

// 計算給定數字的平方
var square = functon(x) {
    return x*x;
}

console.log(square(12)); // 144

var makeNoise = function() {
    console.log("Pling!");
}
makeNoise(); // Pling!

var power = function(base,exponent) {
    var result = 1;
    for(var count = 0;count < exponent;count++) {
        result *= base;
        return result;
    }
}
console.log(power(2,10)); // 1024
複製程式碼

3.2 引數和作用域

函式的引數如同一個普通變數,但其初始值是由函式的呼叫者提供的,而不是函式自身的內部程式碼。

函式有一個重要屬性,就是其內部建立的變數以及引數,都屬於函式的區域性變數。這意味著示例中power函式的result變數會在每次函式呼叫重新建立,這種隔離機制確保了不同函式之間不會相互干擾。

var x = "outside";
var f1 = function() {
    var x = "inside f1";
}
f1();
console.log(x); // outside

var f2 = function() {
    x = "inside f2";
}

f2();
console.log(x); // inside f2
複製程式碼

3.3 巢狀作用域

JavaScript不僅能區分全域性變數和區域性變數,還可以在函式中建立其他函式,併產生不同程度的區域性作用域。

var landscape = function(){
        var result = "";
        var flat = function(size){
            for(var count = 0;count < size;count++){
                result +="_";
            }
        }
        var mountain = function(size) {
            result +="/";
            for(var count = 0;count < size;count++){
                result += "'";
            }
            result += "\\";
        }

        flat(3);
        mountain(4);
        flat(6);
        mountain(1);
        flat(1);
        return result;
    };
   console.log(landscape());
複製程式碼

3.4 函式值

函式變數通常只是充當一段特定程式的名字。這種變數只需要定義一次,而且永遠不會改變。

你不僅可以直接呼叫函式,還可以像使用其他型別的值一樣使用函式型別的值、將其用在任何表示式中、將其存放在新的變數中或者將其作為引數傳遞給其他函式等。同理,一個用於存放函式的變數也只不過是一個普通變數,可以被賦予新的值。

var launchMissiles = function(value) {
    missileSystem.launch("now");
};
if(safeMode){
    launchMissiles = function(value){
        // do something
    }
}
複製程式碼

3.5 符號宣告

相較於使用“var square = function...”表示式來宣告函式,還可以使用另一種更為簡潔的方法宣告函式。

function square(x) {
    return x*x;
}
複製程式碼

從概念上看,函式宣告會被移動到其作用域的頂端,所有作用域內的函式呼叫都不會有任何問題。

console.log("The future says:",future());
function future(){
    return "We STILL have no flying cars."
}

 console.log("The future says:",future1()); // Uncaught TypeError: future1 is not a function
    var future1 = function () {
        return "1111";
    }
複製程式碼

3.6 呼叫棧

function greet(who){
    console.log("Hello" + who);
}
greet("Harry");
console.log("Bye");
複製程式碼

由於函式需要在執行結束後跳轉回撥用該函式的程式碼位置,因此計算機必須記住函式呼叫的上下文。

我們將計算機儲存這個上下文的區域稱之為呼叫棧。每當函式呼叫時,當前的上下文資訊就會被儲存在棧頂。當函式返回時,系統會刪除儲存在棧頂的上下文資訊,並使用該資訊繼續執行程式。

棧需要儲存在計算機記憶體中。若棧儲存的空間過大,計算機就會提示類似於“棧空間溢位”或“遞迴過多”的資訊。

function chicken(){
    return egg();
}
function egg(){
    return chicken();
}

console.log(chicken() + "came first.");
複製程式碼

3.7 可選引數

JavaScript對傳入函式的引數數量幾乎不做任何限制。如果你傳遞了過多引數,多餘的引數就會被忽略掉,而如果你傳遞的引數過少,遺漏的引數將會被賦值成undefined。

該特性的缺點是你可能恰好向函式傳遞了錯誤數量的引數,但沒有人會告訴你這個錯誤。

該特性的優點是我們可以利用這種行為來讓函式接收可選引數。

function power(base,exponent){
    if(exponent == undefined) {
        exponent = 2;
    }
    var result = 1;
    for(var count = 0;count < exponent;count++){
        result *= base;
    }
    return result;
}

console.log(power(4)); // 16
console.log(power(4,3)); // 64
複製程式碼

3.8 閉包

函式可以作為值使用,而且其區域性變數會在每次函式呼叫時重新建立。

function wrapValue(n) {
    var localVariable = n;
    return function() {
        return localVariable;
    }
}

var wrap1 = wrapValue(1);
var wrap2 = wrapValue(2);

console.log(wrap1()); // 1
console.log(wrap2()); // 2
複製程式碼

其實同一變數的多個例項可以同時存在,這也就很好地印證了區域性變數會在每次函式呼叫時重新建立,不同的函式呼叫是不會對其他函式內的區域性變數產生影響的。

我們把這種引用特定的區域性變數例項的功能稱為閉包。一個包裝了一些區域性變數的函式是一個閉包。

function multiplier(factor) {
    return function(number) {
        return number * factor;
    }
}
var twice = multiplier(2);
console.log(twice(5)); // 10
複製程式碼

3.9 遞迴

函式完全可以自己呼叫自己,只要避免棧溢位的問題即可。我們把函式呼叫自身的行為稱為遞迴。

function power(base,exponent){
    if(exponent == 0){
        return 1;
    }else{
        return base * power(base,exponent -1);
    }
}

console.log(power(2,3)); // 8
複製程式碼

在標準的JavaScript實現當中,遞迴寫法的函式執行效率比迴圈寫法的函式慢了大約10倍。執行簡單的迴圈操作比多次函式呼叫效率要高很多。

基本原則:除非程式執行速度確實太慢,否則先不要關注效率問題。一旦出現速度太慢的情況,找出佔用時間最多的部分,然後將其替換成效率更高的程式碼。

但是,我們並不能以偏概全的說遞迴就只是比迴圈的效率低。對於某些問題來說,遞迴相較於迴圈更能解決問題。這類問題通常需要執行和處理多個分支,而每個分支又會引出更多的執行分支。

// 從數字1開始,每次可以將數字加5或乘以3,迴圈執行。這樣可以產生無窮多個新數字。那麼如何編寫函式來找出一個加法乘法序列,以產生指定的數字呢?例如,數字13可以通過1次乘法和2次加法生成,而數字15永遠無法得到。使用遞迴編碼的解決方案如下:

function findSolution(target){
    function find(start,history){
        if(start == target){
            return history;
        }else if(start > target){
            return null;
        }else{
            return find(start + 5,"(" + history + "+5 )") || find(start * 3,"(" + history + "*3 )")
        }
    }
    return find(1,"1");
}

console.log(findSolution(24));
複製程式碼

3.10 新增新函式

這裡有兩種常用的方法將函式引入到程式中。

第一種方法是找出你的程式中多次出現的相似程式碼。我們可以把重複功能的程式碼提取出來放到一個函式中去,並起一個合適的名字。

第二種方法是當你寫一些新功能程式碼,並覺得這些程式碼應該成為一個函式時,我們將這部分程式碼寫到一個函式中,並取一個函式名。

// 編寫一個列印兩個數字的程式,第一個數字是農場中牛的數量,第二個數字是農場中雞的數量,並在數字後面跟上Cows 和Chickens用以說明,並且在兩個數字前填充0,以使得每個數字總是由三位數字組成。

function printFarmInventory(cows,chickens) {
    var cowString = String(cows);
    while(cowString.length < 3){
        cowString = "0" + cowString;
    }
    console.log(cowString + " cows");
    var chickenString = String(chickens);
    while(chickenString.length < 3){
        chickenString = "0" + chickenString;
    }
    console.log(chickenString + " Chickens");
}

printFarmInventory(7,11);

// 擴充套件軟體來輸出豬的數量

function printZeroPaddedWithLabel(number,label){
    var numberString = String(number);
    while(numberString.length < 3){
        numberString = "0" + numberString;
    }
    console.log(numberString + " " + label);
}

function printFarmInventory(cows,chickens,pigs){
    printZeroPaddedWithLabel(cows,"Cows");
    printZeroPaddedWithLabel(chickens,"Chickens");
    printZeroPaddedWithLabel(pigs,"Pigs");
}
printFarmInventory(7,11,3);

// 繼續優化

function zeroPad(number,width){
    var string = String(number);
    while(string.length < width){
        string = "0" + string;
    }
    return string;
}

function printFarmInventory(cows,chickens,pigs){
    console.log(zeroPad(cows,3) + "Cows");
    console.log(zeroPad(chickens,3) + "Chickens");
    console.log(zeroPad(pigs,3) + "pigs");
}
printFarmInventory(7,16,3); 
複製程式碼

比較好的方法是,儘量不要在函式中新增過多功能,除非你真的需要它。我們總是希望能夠編寫一個通用的框架來解決遇到的各類問題。但是別急,這麼做其實並不能解決任何問題,而且不會有人真的去使用這樣的程式碼。

3.11 函式及其副作用

我們可以將函式分成兩類:一類呼叫後產生副作用,而另一類則產生返回值(當然我們也可以定義同時產生副作用和返回值的函式)。

相比於直接產生副作用的函式,產生返回值的函式則更容易整合到新的環境當中使用。

純函式是一種只會產生值而且不會影響它自己範圍外任何事情的函式。

3.12 本章小結

3.13 習題

// 1
function min(a,b){
    if(a> b){
       return b;
    }else{
       return a;
    }
}
console.log(min(1,2));

// 2

function isEven(n){
    if(n == 0){
        return true;
    }else if(n == 1){
        return false;
    }else if (n < 0){
        return isEven(-n);
    }else{
        return isEven(n-2);
    }
}

console.log(isEven(50)); // true
console.log(isEven(75)); // false
console.log(isEven(-1)); // false

// 3

function countBs(str){
    var count = 0;
    for(var i = 0;i < str.length;i++){
        if(str[i] == 'B'){
            count++;
        }
    }
    return count;
}

console.log(countBs("12B45B12B"));

function countChar(str,target){
    var count = 0;
    for(var i = 0; i < str.length;i++){
        if(str[i] == target){
            count++;
        }
    }
    return count;
}
console.log(countChar("12A345AaA","A"));

// 官方正解
function countChar(string, ch) {
  let counted = 0;
  for (let i = 0; i < string.length; i++) {
    if (string[i] == ch) {
      counted += 1;
    }
  }
  return counted;
}

function countBs(string) {
  return countChar(string, "B");
}

console.log(countBs("BBC"));
console.log(countChar("kakkerlak", "k"));
複製程式碼

第4章 資料結構:物件和陣列

數字、布林值和字串構成了基本的資料結構。少了其中任何一樣,你可能都很難構造出完整的結構。我們可以使用物件來把值和其他物件組織起來,通過這種手段來構造更為複雜的結構。

4.1 松鼠人

4.2 資料集

JavaScript提供了一種資料型別,專門用於儲存一系列的值。我們將這種資料型別稱為陣列(array),將一連串的值寫在方括號當中,值之間使用逗號(,)分隔。

var listOfNumbers = [2,3,5,7,11];

console.log(listOfNumbers[1]); // 3
console.log(listOfNumbers[1 -1]); // 2
複製程式碼

4.3 屬性

在JavaScript中,幾乎所有的值都有屬性。但null和undefined沒有。

在JavaScript中有兩種最為常用的訪問屬性的方法:使用點(.)和方括號[]。 如果使用點,則點之後的部分必須是一個合法變數名,即直接寫屬性名稱。如果使用方括號,則JavaScript會將方括號中表示式的返回值作為屬性名稱。

4.4 方法

除了length屬性以外,字串和陣列物件還包含了許多其他屬性,這些屬性是函式值。

var doh = "Doh";
console.log(typeof doh.toUpperCase); // function
console.log(doh.toUpperCase()); // DOH
複製程式碼

我們通常將包含函式的屬性稱為某個值的方法(method)。比如說,“toUpperCase是字串的一個方法”。

var mack = [];
mack.push("Mack");
mack.push("the","knife");
console.log(mack);
console.log(mack.join(" "));
console.log(mack.pop());
console.log(mack);
複製程式碼

我們可以使用push方法向陣列的末尾新增值,pop方法則作用相反:刪除陣列末尾的值,並返回給呼叫者。我們可以使用join方法將字串陣列拼接成單個字串,join方法的引數是連線陣列元素之間的文字內容。

4.5 物件

物件型別的值可以儲存任意型別的屬性,我們可以隨意增刪這些屬性。一種建立物件的方法是使用大括號。

var day1 = {
    squirrel: false,
    events: ["work","touched tree","pizza","running","television"]
};
console.log(day1.squirrel); // false
console.log(day1.wolf); // undefined
day1.wolf = false;
console.log(day1.wolf); // false
複製程式碼

在大括號中,我們可以新增一系列的屬性,並用逗號分隔。每一個屬性均以名稱開頭,緊跟一個冒號,然後是對應屬性的表示式。如果屬性名不是有效的變數名或者數字,則需要使用引號將其括起來。

var description = {
    work: "Went to work",
    "touched tree": "Touched a tree"
}
複製程式碼

讀取一個不存在的屬性就會產生undefined值。

delete是個一元運算子,其運算元是訪問屬性的表示式,可以從物件中移除指定屬性。

var anObject = {
    left: 1,
    right: 2
}
console.log(anObject.left); // 1
delete anObject.left;
console.log(anObject.left); // undefined

console.log("left" in anObject); // false
console.log("right" in anObject); // true
複製程式碼

二元運算子in的第一個運算元是一個表示屬性名的字串,第二個運算元是一個物件,它會返回一個布林值,表示該物件是否包含該屬性。將屬性設定為undefined與使用delete刪除屬性的區別在於:對於第一種情況,物件仍然包含left屬性,只不過該屬性沒有引用任何值;對於第二種情況,物件中已不存在left屬性,因此in運算子會返回false。

陣列只不過是一種用於儲存資料序列的特殊物件,因此typeof[1,2]的執行結果是“Object”。

// 我們可以用一個陣列物件來表示雅克的日誌
var journal = [
{
    events: ["work","touched tree","pizza","running","television"],
    squirrel: false
},
{
    events: ["work","ice cream","cauliflower","lasagna","touched tree","brushed teeth"],
    squirrel: false
}
/* and so on */
];
複製程式碼

4.6 可變性

數字、字串和布林值都是不可變值,我們無法修改這些型別值的內容。

但對於物件來說,我們可以通過修改其屬性來改變物件的內容。

var object1 = {value: 10};
var object2 = object1;
var object3 = {value: 10};

console.log(object1 == object2); // true
console.log(object1 == object3); // false

object1.value = 15;
console.log(object2.value); // 15
console.log(object3.value); // 10
複製程式碼

在JavaScript中,使用==運算子來比較兩個物件時,只有兩個物件引用了同一個值,結果才會返回true。比較兩個不同的物件將會返回false,哪怕物件內容相同。JavaScript中沒有內建深度比較運算子(比較物件內容),但你可以自己編寫一個。

4.7 松鼠人的記錄

var journal = [];

function addEntry(events,didITurnIntoASquirrel){
    journal.push({
        events: events,
        squirrel: didITurnIntoASquirrel
    })
}

addEntry(["work","touched tree","pizza","running","television"],false);
addEntry(["work","ice cream","cauliflower","lasagna","touched tree","brushed teeth"],false);
addEntry(["weekend","cycling","break","peanuts","beer"],true);
複製程式碼

4.8 計算關聯性

// 計算陣列的係數Φ
function phi(table) {
    return (table[3] * table[0] - table[2] * table[1])/Math.sqrt((table[2] + table[3]) * (table[0] + table[1]) * (table[1] + table[3]) * (table[0] + table[2]));
}

console.log(phi([76,9,4,1]));

// 迴圈遍歷整個記錄,並計算出與變身成松鼠相關事件發生的次數。
var JOURNAL = [
  {"events":["carrot","exercise","weekend"],"squirrel":false},
  {"events":["bread","pudding","brushed teeth","weekend","touched tree"],"squirrel":false},
  {"events":["carrot","nachos","brushed teeth","cycling","weekend"],"squirrel":false},
  {"events":["brussel sprouts","ice cream","brushed teeth","computer","weekend"],"squirrel":false},
  {"events":["potatoes","candy","brushed teeth","exercise","weekend","dentist"],"squirrel":false},
  {"events":["brussel sprouts","pudding","brushed teeth","running","weekend"],"squirrel":false},
  {"events":["pizza","brushed teeth","computer","work","touched tree"],"squirrel":false},
  {"events":["bread","beer","brushed teeth","cycling","work"],"squirrel":false},
  {"events":["cauliflower","brushed teeth","work"],"squirrel":false},
  {"events":["pizza","brushed teeth","cycling","work"],"squirrel":false},
  {"events":["lasagna","nachos","brushed teeth","work"],"squirrel":false},
  {"events":["brushed teeth","weekend","touched tree"],"squirrel":false},
  {"events":["lettuce","brushed teeth","television","weekend"],"squirrel":false},
  {"events":["spaghetti","brushed teeth","work"],"squirrel":false},
  {"events":["brushed teeth","computer","work"],"squirrel":false},
  {"events":["lettuce","nachos","brushed teeth","work"],"squirrel":false},
  {"events":["carrot","brushed teeth","running","work"],"squirrel":false},
  {"events":["brushed teeth","work"],"squirrel":false},
  {"events":["cauliflower","reading","weekend"],"squirrel":false},
  {"events":["bread","brushed teeth","weekend"],"squirrel":false},
  {"events":["lasagna","brushed teeth","exercise","work"],"squirrel":false},
  {"events":["spaghetti","brushed teeth","reading","work"],"squirrel":false},
  {"events":["carrot","ice cream","brushed teeth","television","work"],"squirrel":false},
  {"events":["spaghetti","nachos","work"],"squirrel":false},
  {"events":["cauliflower","ice cream","brushed teeth","cycling","work"],"squirrel":false},
  {"events":["spaghetti","peanuts","computer","weekend"],"squirrel":true},
  {"events":["potatoes","ice cream","brushed teeth","computer","weekend"],"squirrel":false},
  {"events":["potatoes","ice cream","brushed teeth","work"],"squirrel":false},
  {"events":["peanuts","brushed teeth","running","work"],"squirrel":false},
  {"events":["potatoes","exercise","work"],"squirrel":false},
  {"events":["pizza","ice cream","computer","work"],"squirrel":false},
  {"events":["lasagna","ice cream","work"],"squirrel":false},
  {"events":["cauliflower","candy","reading","weekend"],"squirrel":false},
  {"events":["lasagna","nachos","brushed teeth","running","weekend"],"squirrel":false},
  {"events":["potatoes","brushed teeth","work"],"squirrel":false},
  {"events":["carrot","work"],"squirrel":false},
  {"events":["pizza","beer","work","dentist"],"squirrel":false},
  {"events":["lasagna","pudding","cycling","work"],"squirrel":false},
  {"events":["spaghetti","brushed teeth","reading","work"],"squirrel":false},
  {"events":["spaghetti","pudding","television","weekend"],"squirrel":false},
  {"events":["bread","brushed teeth","exercise","weekend"],"squirrel":false},
  {"events":["lasagna","peanuts","work"],"squirrel":true},
  {"events":["pizza","work"],"squirrel":false},
  {"events":["potatoes","exercise","work"],"squirrel":false},
  {"events":["brushed teeth","exercise","work"],"squirrel":false},
  {"events":["spaghetti","brushed teeth","television","work"],"squirrel":false},
  {"events":["pizza","cycling","weekend"],"squirrel":false},
  {"events":["carrot","brushed teeth","weekend"],"squirrel":false},
  {"events":["carrot","beer","brushed teeth","work"],"squirrel":false},
  {"events":["pizza","peanuts","candy","work"],"squirrel":true},
  {"events":["carrot","peanuts","brushed teeth","reading","work"],"squirrel":false},
  {"events":["potatoes","peanuts","brushed teeth","work"],"squirrel":false},
  {"events":["carrot","nachos","brushed teeth","exercise","work"],"squirrel":false},
  {"events":["pizza","peanuts","brushed teeth","television","weekend"],"squirrel":false},
  {"events":["lasagna","brushed teeth","cycling","weekend"],"squirrel":false},
  {"events":["cauliflower","peanuts","brushed teeth","computer","work","touched tree"],"squirrel":false},
  {"events":["lettuce","brushed teeth","television","work"],"squirrel":false},
  {"events":["potatoes","brushed teeth","computer","work"],"squirrel":false},
  {"events":["bread","candy","work"],"squirrel":false},
  {"events":["potatoes","nachos","work"],"squirrel":false},
  {"events":["carrot","pudding","brushed teeth","weekend"],"squirrel":false},
  {"events":["carrot","brushed teeth","exercise","weekend","touched tree"],"squirrel":false},
  {"events":["brussel sprouts","running","work"],"squirrel":false},
  {"events":["brushed teeth","work"],"squirrel":false},
  {"events":["lettuce","brushed teeth","running","work"],"squirrel":false},
  {"events":["candy","brushed teeth","work"],"squirrel":false},
  {"events":["brussel sprouts","brushed teeth","computer","work"],"squirrel":false},
  {"events":["bread","brushed teeth","weekend"],"squirrel":false},
  {"events":["cauliflower","brushed teeth","weekend"],"squirrel":false},
  {"events":["spaghetti","candy","television","work","touched tree"],"squirrel":false},
  {"events":["carrot","pudding","brushed teeth","work"],"squirrel":false},
  {"events":["lettuce","brushed teeth","work"],"squirrel":false},
  {"events":["carrot","ice cream","brushed teeth","cycling","work"],"squirrel":false},
  {"events":["pizza","brushed teeth","work"],"squirrel":false},
  {"events":["spaghetti","peanuts","exercise","weekend"],"squirrel":true},
  {"events":["bread","beer","computer","weekend","touched tree"],"squirrel":false},
  {"events":["brushed teeth","running","work"],"squirrel":false},
  {"events":["lettuce","peanuts","brushed teeth","work","touched tree"],"squirrel":false},
  {"events":["lasagna","brushed teeth","television","work"],"squirrel":false},
  {"events":["cauliflower","brushed teeth","running","work"],"squirrel":false},
  {"events":["carrot","brushed teeth","running","work"],"squirrel":false},
  {"events":["carrot","reading","weekend"],"squirrel":false},
  {"events":["carrot","peanuts","reading","weekend"],"squirrel":true},
  {"events":["potatoes","brushed teeth","running","work"],"squirrel":false},
  {"events":["lasagna","ice cream","work","touched tree"],"squirrel":false},
  {"events":["cauliflower","peanuts","brushed teeth","cycling","work"],"squirrel":false},
  {"events":["pizza","brushed teeth","running","work"],"squirrel":false},
  {"events":["lettuce","brushed teeth","work"],"squirrel":false},
  {"events":["bread","brushed teeth","television","weekend"],"squirrel":false},
  {"events":["cauliflower","peanuts","brushed teeth","weekend"],"squirrel":false}
];

function hasEvent(event,entry) {
    return entry.events.indexOf(event) != -1;
}

function tableFor(event,journal) {
    var table = [0,0,0,0];
    for(var i = 0; i < journal.length; i++){
        var entry = journal[i];
        var index = 0;
        if(hasEvent(event,entry)){
            index += 1;
        }
        if(entry.squirrel){
            index += 2;
        }
        table[index] += 1;
    }
    return table;
}
console.log(tableFor("pizza",JOURNAL));
複製程式碼

4.9 物件對映

對映表(map)可以通過一個值(在本例中是事件名)來獲取對應的另一個值(在本例中是Φ係數)。

var map = {};
function storePhi(event,phi) {
    map[event] = phi;
}
storePhi("pizza",0.069);
storePhi("touched tree",-0.081);
console.log("pizza" in map); // true
console.log(map["touched tree"]); // -0.081
複製程式碼

JavaScript提供了另一種遍歷物件屬性的迴圈語句。它與一般的for迴圈看起來很像,只是我們使用的關鍵字不是for而是in。

for(var event in map) {
    console.log("The correlation for '" + event + "' is " + map[event]);
}

// The correlation for 'pizza' is 0.069
// The correlation for 'touched tree' is -0.081
複製程式碼

4.10 分析結果

為了找出資料集中存在的所有事件型別,我們只需依次處理每條記錄,然後遍歷記錄中的所有事件即可。

function gatherCorrelations(journal) {
    var phis = {};
    for(var entry = 0;entry < journal.length;entry++){
        var events = journal[entry].events;
        for(var i = 0; i < events.length;i++){
            var event = events[i];
            if(!(event in phis)){
                phis[event] = phi(tableFor(event,journal));
            }
        }
    }
    return phis;
}

var correlations = gatherCorrelations(JOURNAL);
console.log(correlations.pizza);

for(var event in correlations) {
    console.log(event + ":" + correlations[event]);
}

for(var event in correlations) {
    var correlation = correlations[event];
    if(correlation > 0.1 || correlation < -0.1){
        console.log(event + ":" + correlation);
    }
}

for(var i = 0;i < JOURNAL.length;i++){
    var entry = JOURNAL[i];
    if(hasEvent("peanuts",entry) && !hasEvent("brushed teeth",entry)){
        entry.events.push("peanut teeth");
    }
}

console.log(phi(tableFor("peanut teeth",JOURNAL))); // 1
複製程式碼

4.11 詳解陣列

一些實用的陣列方法

push和pop,分別用於在陣列末尾新增或刪除元素。

unshift和shift,分別用於在陣列的開頭新增或刪除元素。

var todoList = [];
function rememberTo(task) {
    todoList.push(task);
}

function whatIsNext() {
    return todoList.shift();
}

function urgentlyRememberTo(task) {
    todoList.unshift(task);
}
複製程式碼

indexOf, 從陣列第一個元素向後搜尋。

lastIndexOf,從陣列最後一個元素向前搜尋。

indexOf和lastIndexOf方法都有一個可選引數,可以用來指定搜尋的起始位置。

console.log([1,2,3,2,1].indexOf(2)); // 1
console.log([1,2,3,2,1].lastIndexOf(2)); // 3
複製程式碼

slice,該方法接受一個起始索引和一個結束索引,然後返回陣列中兩個索引範圍內的元素。起始索引元素包含在返回結果中,但結束索引元素不會包含在返回結果中。如果沒有指定結束索引,slice會返回從起始位置之後的所有元素。對於字串來說,它也有一個具有相同功能的slice方法供開發人員使用。

console.log([0,1,2,3,4].slice(2,4)); // [2,3]
console.log([0,1,2,3,4].slice(2)); // [2,3,4]
複製程式碼

concat 方法用於拼接兩個陣列,其作用類似於字串的+運算子。

function remove(array,index) {
    return array.slice(0,index).concat(array.slice(index + 1 ));
}

console.log(remove(["a","b","c","d","e"],2)); // ["a","b","d","e"]
複製程式碼

4.12 字串及其屬性

我們可以呼叫字串的length或toUpperCase這樣的屬性,但不能向字串中新增任何新的屬性。

var myString = "Fido";
myString.myProperty = "value";
console.log(myString.myProperty); // undefined
複製程式碼

字串、數字和布林型別的值並不是物件,因此當你向這些值中新增屬性時JavaScript並不會報錯,但實際上你並沒有將這些屬性新增進去。這些值都是不可變的,而且無法向其中新增任何屬性。

但這些型別的值包含一些內建屬性。每個字串中包含了若干方法供我們使用,最有用的方法可能就是slice和indexOf了,它們的功能與陣列中的同名方法類似。

console.log("coconuts".slice(4,7)); // nut
console.log("coconuts".indexOf("u")); // 5
複製程式碼

唯一的區別在於,字串的indexOf方法可以使用多個字元作為搜尋條件,而陣列中的indexOf方法則只能搜尋單個元素。

console.log("one two three".indexOf("ee")); // 11
複製程式碼

trim方法用於刪除字串中開頭和結尾的空白符號(空格、換行和製表符等符號)。

console.log("   okay \n  ".trim()); // okay
複製程式碼
// 獲取字串中某個特定的字元
var string = "abc";
console.log(string.length); // 3
console.log(string.charAt(0)); // a
console.log(string[1]); // b
複製程式碼

4.13 arguments物件

每當函式被呼叫時,就會在函式體的執行環境當中新增一個特殊的變數arguments。該變數指向一個包含了所有入參的物件。在JavaScript中,我們可以傳遞多於(或少於)函式引數列表定義個數的引數。

function noArguments(){};
noArguments(1,2,3); // This is okay

function threeArgumnents(a,b,c){};
threeArguments(); // And so is this
複製程式碼

arguments物件有一個length屬性,表示實際傳遞給函式的引數個數。每個引數對應一個屬性,被命名為0,1,2,以此類推。

function argumentCounter() {
    console.log("You gave me",arguments.length,"arguments.");
}
argumentCounter("Straw man","Tautology","Ad hominem"); // You gave me 3 arguments.
複製程式碼
function addEntry(squirrel){
    var entry = {
        events: [],
        squirrel: squirrel
    };
    for(var i = 1; i < arguments.length; i++){
        entry.events.push(arguments[i]);
    }
    journal.push(entry);
}
addEntry(true,"work","touched tree","pizza","running","television");
複製程式碼

4.14 Math物件

Math物件簡單地把一組相關的功能打包成一個物件供使用者使用。全域性只有一個Math物件,其物件本身沒有什麼實際用途。Math物件其實提供了一個“名稱空間”,封裝了所有的數學運算函式和值,確保這些元素不會變成全域性變數。

過多的全域性變數會對名稱空間造成“汙染”。全域性變數越多,就越有可能一不小心把某些變數的值覆蓋掉。

function randomPointOnCircle(radius) {
    var angle = Math.random() * 2 * Math.PI;
    return {
        x: radius * Math.cos(angle),
        y: radius * Math.sin(angle)
    };
}

 console.log(randomPointOnCircle(2));
複製程式碼

Math.random,每次呼叫該函式時,會返回一個偽隨機數,範圍在0(包括) ~ 1(不包括)之間。

console.log(Math.random());
console.log(Math.random());
console.log(Math.random());
複製程式碼

Math.floor,向下取整

Math.ceil,向上取整

Math.round,四捨五入

console.log(Math.floor(Math.random() * 10)); // 等概率地取到0~9中的任何一個數字。
複製程式碼

4.15 全域性物件

JavaScript全域性作用域中有許多全域性變數,都可以通過全域性物件進行訪問。每一個全域性變數作為一個屬性儲存在全域性物件當中。在瀏覽器中,全域性物件儲存在window變數當中。

var myVar = 10;
console.log("myVar" in window); // true
console.log(window.myVar); // 10
複製程式碼

4.16 本章小結

物件和陣列(一種特殊物件)可以將幾個值組合起來形成一個新的值。

在JavaScript中,除了null和undefined以外,絕大多數的值都含有屬性。

在陣列中有一些具名屬性,比如length和一些方法。

物件可以用作對映表,將名稱與值關聯起來。我們可以使用in運算子確定物件中是否包含特定名稱的屬性。我們同樣可以在for迴圈中(for(var name in object))使用關鍵字in來遍歷物件中包含的屬性。

4.17 習題

// 1 編寫一個range函式,接受兩個引數:start和end,然後返回包含start到end(包括end)之間的所有數字。
function range(start,end){
var arr = [];
if(start > end){
     for(var i = start; i >= end;i--){
        arr.push(i);
    }
}else{
     for(var i = start; i <= end;i++){
        arr.push(i);
    }
}
    return arr;
}
console.log(range(1,10));
console.log(range(5,2,-1));

// 2 編寫一個sum函式,接受一個數字陣列,並返回所有數字之和
function sum(arr){
    var sum = 0;
    for(var i = 0; i < arr.length;i++){
        sum += arr[i];
    }
    return sum;
}

console.log(sum(range(1, 10)));

// 3 附加題修改range函式,接受第3個可選引數,指定構建陣列時的步數(step)。

 function range(start,end,step){
        var arr = [];
        if(step == undefined){
            step = 1;
        }
        if(start > end){
            for(var i = start; i >= end;i= i + step){
                arr.push(i);
            }
        }else{
            for(var i = start; i <= end;i = i + step){
                arr.push(i);
            }
        }
        return arr;
    }
    console.log(range(1,10,2));
    console.log(range(5,2,-1));
    
// 官方正解
function range(start, end, step = start < end ? 1 : -1) {
  let array = [];

  if (step > 0) {
    for (let i = start; i <= end; i += step) array.push(i);
  } else {
    for (let i = start; i >= end; i += step) array.push(i);
  }
  return array;
}

function sum(array) {
  let total = 0;
  for (let value of array) {
    total += value;
  }
  return total;
}

// 逆轉陣列
function reverseArray(arr){
    var output = [];
    for(var i = arr.length-1;i >= 0;i--){
        output.push(arr[i]);
    }
    return output;
}

console.log(reverseArray([1,2,3,4,5]));

// 逆轉陣列2
    function reverseArrayInPlace(array){
        for(let i = 0; i < Math.floor(array.length / 2);i++){
            let old = array[i];
            array[i] = array[array.length -1-i];
            array[array.length-1-i]= old;
        }
        return array;
    }

    console.log(reverseArrayInPlace([1,2,3,4,5]));

// 實現列表
var list = {
    value: 1,
    rest: {
        value: 2,
        rest: {
            value: 3,
            rest: null
        }
    }
}


function arrayToList(array) {
  let list = null;
  for (let i = array.length - 1; i >= 0; i--) {
    list = {value: array[i], rest: list};
  }
  return list;
}

console.log(arrayToList([1,2,3]));

// 列表轉換成陣列
function listToArray(list) {
  let array = [];
  for (let node = list; node; node = node.rest) {
    array.push(node.value);
  }
  return array;
}

function listToArray(list){
    let array = [];
    for(let node = list;node;node = node.rest){
        array.push(node.value);
    }
    return array;
}

console.log(listToArray(list));

// 建立一個新的列表
function prepend(value,list) {
    return {
        value,
        rest: list
    };
}

// 返回列表中指定位置的元素
function nth(list, n) {
  if (!list) return undefined;
  else if (n == 0) return list.value;
  else return nth(list.rest, n - 1);
}

function nth(list,n) {
    if(!list){
        return undefined;
    }else if(n == 0){
        return list.value;
    }else {
        return nth(list.rest,n-1);
    }
}

console.log(list,3);

// 深度比較 編寫一個函式deepEqual,接受兩個引數,若兩個物件是同一個值或兩個物件中有相同屬性,且使用deepEqual比較屬性值均返回true時,返回true
function deepEqual(a, b) {
  if (a === b) return true;
  
  if (a == null || typeof a != "object" ||
      b == null || typeof b != "object") return false;

  let keysA = Object.keys(a), keysB = Object.keys(b);

  if (keysA.length != keysB.length) return false;

  for (let key of keysA) {
    if (!keysB.includes(key) || !deepEqual(a[key], b[key])) return false;
  }

  return true;
}

複製程式碼

第5章 高階函式

讓我們簡單回顧一下前言當中的兩個示例。其中第一個程式包含了6行程式碼並可以直接執行。

var total = 0,count = 1;
while(count <= 10){
    total += count;
    count += 1;
}
console.log(total);
複製程式碼

第二個程式則依賴於外部函式才能執行,且只有一行程式碼。

console.log(sum(range(1,10)));
複製程式碼

第二個程式編寫的程式碼很好地表達了我們期望解決的問題。相比於將這些程式碼直接寫到一起,這種表述方式更為簡單,同時也易於避免錯誤。

5.1 抽象

在程式設計中,我們把這種編寫程式碼的方式稱為抽象。抽象可以隱藏底層的實現細節,從更高(或更加抽象)的層次看待我們要解決的問題。

作為一名程式設計師,我們需要具備在恰當時候將程式碼抽象出來,形成一個新的函式或概念的能力。

5.2 陣列遍歷抽象

// 1.將陣列中的每個元素列印到控制檯
var array = [1,2,3];
for(var i = 0; i < array.length;i++){
    var current = array[i];
    console.log(current);
}

// 2.將1抽象成一個函式
function logEach(array){
    for(var i = 0; i < array.length;i++){
    console.log(array[i]);
}
}

function forEach(array,action){
    for(var i = 0; i < array.length;i++){
    action(array[i]);
}
}
forEach(["Wampeter","Foma","Granfalloon"],console.log);

// 3.通常來說,我們不會給forEach傳遞一個預定義的函式,而是直接新建一個函式值。
var numbers = [1,2,3,4,5],sum = 0;
forEach(numbers,function(number){
    sum += number;
});
console.log(sum);
複製程式碼

實際上,我們不需要自己編寫forEach函式,該函式其實是陣列的一個標準方法。

function gatherCorrelations(journal) {
    var phis = {};
    for(var entry = 0;entry < journal.length;entry++){
        var events = journal[entry].events;
        for(var i = 0; i < events.length;i++){
            var event = events[i];
            if(!(event in phis)){
                phis[event] = phi(tableFor(event,journal));
            }
        }
    }
    return phis;
}
// 使用forEach改寫上面的程式碼
function gatherCorrelations(journal){
    var phis = {};
    journal.forEach(function(entry){
        entry.events.forEach(function (event){
             if(!(event in phis)){
                phis[event] = phi(tableFor(event,journal));
            }
        })
    })
    return phis;
}
複製程式碼

5.3 高階函式

如果一個函式操作其他函式,即將其他函式作為引數或將函式作為返回值,那麼我們可以將其稱為高階函式。

我們可以使用高階函式對一系列操作和值進行抽象。

// 使用高階函式新建另一些函式
function greaterThan(n) {
    return function(m){
        return m > n;
    }
}
var greaterThan10 = greaterThan(10);
console.log(greaterThan10(11)); // true

// 使用高階函式來修改其他函式
function noisy(f) {
    return function(arg){
        console.log("calling with",arg);
        var val = f(arg);
        console.log("called with",arg,"-got",val);
        return val;
    }
}
noisy(Boolean)(0);
// calling with 0
// called with 0 -got false

// 使用高階函式來實現新的控制流
function unless(test,then) {
    if(!test){
        then();
    }
}

function repeat(times,body){
    for(var i = 0;i < times;i++){
        body(i);
    }
}
repeat(3,function(n){
    unless(n % 2,function (){
        console.log(n,"is even");
    })
})
複製程式碼

5.4 引數傳遞

我們在前面定義的函式noisy會把引數傳遞給另一個函式使用,這會導致一個相當嚴重的問題。

function noisy(f) {
    return function(arg){
        console.log("calling with",arg);
        var val = f(arg);
        console.log("called with",arg,"-got",val);
        return val;
    }
}
複製程式碼

如果函式f接受多個引數,那麼該函式只能接受第一個引數。函式f沒有辦法知道呼叫者傳遞給noisy的引數個數。

JavaScript函式的apply方法可以解決這個問題。

function transparentWrapping(f){
    return function(){
        return f.apply(null,arguments);
    }
}
複製程式碼

這裡的transparentWrapping函式沒有什麼實際用處,但該函式返回的內部函式可以將使用者指定的引數全部傳遞給f。

5.5 JSON

[
{
   "name": "Emma de Milliano",
   "sex": "f",
   "born": 1876,
   "died": 1956,
   "father": "Petrus de Milliano",
   "mother": "Sophia van Damme"
},
{
   "name": "Carolus",
   "sex": "m",
   "born": 1832,
   "died": 1905,
   "father": "Carel Haverbeke",
   "mother": "Maria van Brussel"
}
... and so on
]
複製程式碼

這種格式是JSON格式,即JavaScript Object Notation的縮寫。該格式廣泛用於資料儲存和web通訊。

所有屬性名都必須用雙引號括起來,而且只能使用簡單的資料表示式,不能填寫函式呼叫、變數以及任何含有實際計算過程的程式碼。

JavaScript提供了JSON.stringify函式,用於將資料轉換成該格式。還有JSON.parse函式,用於將該格式資料轉換成原有的資料型別。

var string = JSON.stringify({ name: "X",born: 1980});
console.log(string); // {"name":"X","born":1980}
console.log(JSON.parse(string).born); // 1980
複製程式碼

5.6 陣列過濾

function filter(array,test){
    var passed = [];
    for(var i = 0; i < array.length;i++){
        if(test(array[i])){
            passed.push(array[i]);
        }
    }
    return passed;
}
console.log(filter(ancestry,function(person) {
    return person.born > 1990 && person.born < 1925
}))
複製程式碼

該函式使用test函式作為引數來實現過濾操作。我們對陣列中的每個元素呼叫test函式,並通過返回值來確定當前元素是否滿足條件。

與forEach一樣,filter函式也是陣列中提供的一個標準方法。本例中定義的函式只是用於展示內部實現原理。

console.log(ancestry.filter(function (person){
    return person.father == "Carel Haverbeke";
}))
複製程式碼

5.7 使用map函式轉換陣列

map方法可以對陣列中的每個元素呼叫函式,然後利用返回值來構建一個新的陣列,實現轉換陣列的操作。

function map(array,transform){
    var mapped = [];
    for(var i = 0;i < array.length;i++){
        mapped.push(transform(array[i]);
    }
    return mapped;
}

var overNinety = ancestry.filter(function(person){
    return person.died - person.born > 90;
})

console.log(map(overNinety,function(person){
    return person.name;
}))
複製程式碼

與forEach和filter一樣,map也是陣列中的一個標準方法。

5.8 使用reduce進行資料彙總

根據整個陣列計算出一個值。

reduce函式包含三個引數:陣列、執行合併操作的函式和初始值。

function reduce(array,combine,start) {
    var current = start;
    for(var i = 0;i < array.length;i++){
        current = combine(current,array[i]);
    }
    return current;
}

console.log(reduce([1,2,3,4],function(a,b){
    return a+b;
},0))
複製程式碼

陣列中有一個標準的reduce方法,當然和我們上面看到的那個函式一致,可以簡化合並操作。

console.log(ancestry.reduce(function(min,cur){
    if(cur.born < min.born){
        return cur;
    }else{
        return min;
    }
}))
複製程式碼

5.9 可組合性

// 在不使用高階函式的情況下,實現以上示例

var min = ancestry[0];
for(var i = 1;i < ancestry.length;i++){
    var cur = ancestry[i];
    if(cur.born < min.born){
        min = cur;
    }
}
console.log(min);
複製程式碼

這段程式碼中多了一些變數,雖然多了兩行程式碼,但程式碼邏輯還是很容易讓人理解的。

當你遇到需要組合函式的情況時,高階函式的價值就突顯出來了。舉個例子,編寫一段程式碼,找出資料集中男人和女人的平均年齡。

function average(array) {
    function plus(a,b){
        return a+b;
    }
    return array.reduce(plus) / array.length;
}

function age(p){
    return p.died -p.born;
}

function male(p){
    return p.sex == "m";
}

function female(p){
    return p.sex == "f";
}

console.log(average(ancestry.filter(male).map(age)));
console.log(average(ancestry.filter(female).map(age)));
複製程式碼

這段程式碼並沒有將邏輯放到整個迴圈體中,而是將邏輯巧妙地組合成了我們所關注的幾個方面:判斷性別、計算年齡和計算平均數。

我們可以採用這種方式編寫出邏輯清晰的程式碼。不過,編寫這樣的程式碼也是有代價的。

5.10 效能開銷

將函式傳遞給forEach來處理陣列迭代任務的確十分方便而且易於閱讀。但JavaScript中函式呼叫卻比簡單的迴圈結構代價更高。

當程式執行很慢的時候,問題往往只是巢狀最深的迴圈體中的一小部分程式碼引起的。

5.11 曾曾曾曾......祖父

// 構建一個物件,將祖輩的姓名與表示人的物件關聯起來
var byName = {};
ancestry.forEach(function(person){
    byName[person.name] = person;
})
console.log(byName["Philibert Haverbeke"]);

// 編寫reduceAncestors函式,用於從家譜樹中提煉出一個值。
function reduceAncestors(person,f,defaultValue){
    function valueFor(person){
        if(person == null){
            return defaultValue;
        }else{
            return f(person,valueFor(byName[person.mother]),valueFor(byName[person.father]));
        }
    }
    return valueFor(person);
}

function sharedDNA(person,formMother,formFather){
    if(person.name == "Pauwels van Haverbeke"){
        return 1;
    }else{
        return (formMother + formFather) / 2;
    }
}

var ph = byName["Philibert Haverbeke"];
console.log(reduceAncestors(ph,sharedDNA,0) / 4);

// 找出滿足特定條件的祖先比例,比如可以查詢年齡超過70歲的人

function countAncestors(person,test){
    function combine(person,formMother,formFather){
        var thisOneCounts = test(person);
        return fromMather + formFather + (thisOneCounts ? 1 : 0);
    }
    return reduceAncestors(person,combine,0);
}

function longLivingPercentage(person){
    var all = countAncestors(person,function(person){
        return true;
    })
    var longLiving = countAncestors(person,function(person){
        return (person.died - person.born) >= 70;
    })
    return longLiving / all;
}
console.log(longLivingPercentage(byName(["Emile Haverbeke"]));
複製程式碼

5.12 繫結

每個函式都有一個bind方法,該方法可以用來建立新的函式,稱為繫結函式。

var theSet = ["Carel Haverbeke","Maria van Brussel","Donald Duke"];

function isInSet(set,person){
    return set.indexOf(person.name) > -1;
}
console.log(ancestry.filter(function(person){
    return isInset(theSet,person);
}))

console.log(ancestry.filter(isInSet.bind(null,theSet))); // same result
複製程式碼

呼叫bind會返回一個新的函式,該函式呼叫isInSet時會將theSet作為第一個引數,並將傳遞給該函式的剩餘引數一起傳遞給isInSet。

5.13 本章小結

將函式型別的值傳遞給其他函式不僅十分有用,而且還是JavaScript中一個重要的功能。我們可以在編寫函式的時候把某些特定的操作預留出來,並在真正的函式呼叫中將具體的操作作為函式傳遞進來,實現完整的計算過程。

陣列中提供了很多實用的高階函式,其中forEach用於遍歷陣列元素,實現某些特定的功能。filter用於過濾掉一些元素,構造一個新陣列。map會構建一個新陣列,並通過一個函式處理每個元素,將處理結果放入新陣列中。reduce則將陣列元素最終歸納成一個值。

函式物件有一個apply方法,我們可以通過該方法呼叫函式,並使用陣列來指定函式引數。另外還有一個bind方法,它用於建立一個新函式,並預先確定其中一部分引數。

5.14 習題

// 1.陣列降維
// 結合使用reduce與concat方法,將輸入的二維陣列(陣列的陣列)中的元素提取出來,並存放到一個一維陣列中

var arrays = [[1, 2, 3], [4, 5], [6]];

console.log(arrays.reduce(function(flat, current) {
  return flat.concat(current);
}, []));

// 2.計算母子年齡差
function average(array) {
  function plus(a, b) { return a + b; }
  return array.reduce(plus) / array.length;
}

var byName = {};
ancestry.forEach(function(person) {
  byName[person.name] = person;
});

var differences = ancestry.filter(function(person) {
  return byName[person.mother] != null;
}).map(function(person) {
  return person.born - byName[person.mother].born;
});

console.log(average(differences));

// 3.計算平均壽命
function average(array) {
  function plus(a, b) { return a + b; }
  return array.reduce(plus) / array.length;
}

function groupBy(array, groupOf) {
  var groups = {};
  array.forEach(function(element) {
    var groupName = groupOf(element);
    if (groupName in groups)
      groups[groupName].push(element);
    else
      groups[groupName] = [element];
  });
  return groups;
}

var byCentury = groupBy(ancestry, function(person) {
  return Math.ceil(person.died / 100);
});

for (var century in byCentury) {
  var ages = byCentury[century].map(function(person) {
    return person.died - person.born;
  });
  console.log(century + ": " + average(ages));
}

// 4.使用every和some方法
function every(array, predicate) {
  for (var i = 0; i < array.length; i++) {
    if (!predicate(array[i]))
      return false;
  }
  return true;
}

function some(array, predicate) {
  for (var i = 0; i < array.length; i++) {
    if (predicate(array[i]))
      return true;
  }
  return false;
}

console.log(every([NaN, NaN, NaN], isNaN));
// → true
console.log(every([NaN, NaN, 4], isNaN));
// → false
console.log(some([NaN, 3, 4], isNaN));
// → true
console.log(some([2, 3, 4], isNaN));
// → false
複製程式碼

第6章 深入理解物件

6.1 歷史

6.2 方法

方法只是引用了函式值的屬性。以下是一個簡單的方法:

var rabbit = {};
rabbit.speak = function(line){
    console.log("The rabbit says '" + line + "'");
}
rabbit.speak("I'm alive."); // The rabbit says 'I'm alive.'
複製程式碼

方法通常會在物件被呼叫時執行一些操作。將函式作為物件的方法呼叫時,會找到物件中對應的屬性並直接呼叫。在呼叫object.method()時,物件中的一個特殊變數this會指向當前方法所屬的物件。

function speak(line){
    console.log("The " + this.type + " rabbit says '" + line + "'");
}
var whiteRabbit = { type: "white",speak: speak };
var fatRabbit = { type: "fat",speak: speak };
whiteRabbit.speak("Oh my ears and whiskers, " + "how late it's getting!");
fatRabbit.speak("I could sure use a carrot right now.");
// The white rabbit says 'Oh my ears and whiskers, how late it's getting!'
// The fat rabbit says 'I could sure use a carrot right now.'
複製程式碼

這段程式碼使用了關鍵字this來輸出正在說話的兔子的種類。我們回想一下apply和bind方法,這兩個方法接受的第一個引數可以用來模擬物件中方法的呼叫。這兩個方法會把第一個引數複製給this。

函式有一個call方法,類似於apply方法。該方法也可以用於函式呼叫,但該方法會像普通函式一樣接受引數,我們不需要將引數放到陣列中。和apply和bind方法一樣,你也可以向call方法傳遞一個特定的this值。

speak.apply(fatRabbit,["Burp!"]); // The fat rabbit says 'Burp!'
speak.call({type: "old"},"Oh my."); // The old rabbit says 'Oh my.'
複製程式碼

6.3 原型

我們來仔細看看以下這段程式碼。

var empty = {};
console.log(empty.toString); // ƒ toString() { [native code] }
console.log(empty.toString()); // [object Object]
複製程式碼

每個物件除了擁有自己的屬性外,幾乎都包含一個原型(prototype)。原型是另一個物件,是物件的一個屬性來源。當開發人員訪問一個物件不包含的屬性時,就會從物件原型中搜尋屬性,接著是原型的原型,以此類推。

那麼空物件的原型是什麼呢?是Object.prototype,它是所有物件中原型的父原型。

console.log(Object.getPrototypeOf({}) == Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null
複製程式碼

JavaScript物件原型的關係是一種樹形結構,整個樹形結構的根部就是Object.prototype。Object.prototype提供了一些可以在所有物件中使用的方法。比如說,toString方法可以將一個物件轉換成其字串表示形式。

許多物件並不直接將Object.prototype作為其原型,而會使用另一個原型物件,用於提供物件自己的預設屬性。函式繼承自Function.prototype,而陣列繼承自Array.prototype。

console.log(Object.getPrototypeOf(isNaN) == Function.prototype); // true
console.log(Object.getPrototypeOf([]) == Array.prototype); // true
複製程式碼

對於這樣的原型物件來說,其自身也包含了一個原型物件,通常情況下是Object.prototype,所以說,這些原型物件可以間接提供toString這樣的方法。

var protoRabbit = {
    speak: function(line){
        console.log("The " + this.type + " rabbit says '" + line +"'");
    }
}

var killerRabbit = Object.create(protoRabbit);
killerRabbit.type = "killer";
killerRabbit.speak("SKREEEE!"); // The killerrabbit says 'SKREEEE!'
複製程式碼

原型物件protoRabbit是一個容器,用於包含所有兔子物件的公有屬性。每個獨立的兔子物件(比如killerRabbit)可以包含其自身屬性(比如本例中的type屬性),也可以派生其原型物件中公有的屬性。

6.4 建構函式

在JavaScript中,呼叫函式之前新增一個關鍵字new則表示呼叫其建構函式。建構函式中包含了指向新物件的變數this,除非建構函式顯式地返回了另一個物件的值,否則建構函式會返回這個新建立的物件。

通過關鍵字new建立的物件稱之為建構函式的例項。

這裡給出一個簡單的用於建立rabbit的建構函式。建構函式的名稱一般以大寫字母開頭。

function Rabbit(type){
    this.type = type;
}
var killerRabbit = new Rabbit("killer");
var blackRabbit = new Rabbit("black");
console.log(blackRabbit.type); // black
複製程式碼

對於建構函式來說(實際上,對所有函式適用),都會自動獲得一個名為prototype的屬性。在預設情況下,該屬性是一個普通的派生自Object.prototype的空物件。所有使用特定建構函式建立的物件都會將建構函式的prototype屬性作為其原型。因此,我們可以很容易地為所有使用Rabbit建構函式建立的物件新增speak方法。

Rabbit.prototype.speak = function(line){
    console.log("The " + this.type + " rabbit says '" + line +"'");
}
blackRabbit.speak("Doom...");
複製程式碼

建構函式其實就是函式,因此其實際原型是Function.prototype。而建構函式的prototype屬性則是其所建立的例項的原型,而非建構函式自身的原型。

6.5 覆蓋繼承的屬性

當你向一個物件新增屬性時,無論該屬性是否已經存在於物件原型中,該屬性都會被新增到這個物件中去,並作為物件自己的屬使用。如果原型中存在同名屬性,那麼在呼叫該屬性時,就不會再呼叫原型中的那個屬性了,轉而呼叫我們新增到物件中的屬性。但原型本身不會被修改。

Rbbit.prototype.teeth = "small";
console.log(killerRabbit.teeth); // small
killerRabbit.teeth = "long, sharp, and bloody";
console.log(blackRabbit.teeth); // small
console.log(killerRabbit.teeth); // long, sharp, and bloody
console.log(Rabbit.prototype.teeth); // small
複製程式碼

覆蓋原型中存在的屬性是很有用的一個特性。

我們也可以為標準函式和陣列原型提供一個不同於Object原型的toString方法。

console.log(Array.prototype.toString == Object.prototype.toString); // false
console.log([1,2].toString()); // 1,2
複製程式碼

直接使用陣列呼叫Object.prototype.toString則會產生一個完全不同的字串。

console.log(Object.prototype.toString.call([1,2])); // [object Array] 
ps:檢測物件型別的最佳方式
複製程式碼

6.6 原型汙染

我們隨時都可以使用原型物件新增新的屬性和方法。

Rabbit.prototype.dance = function(){
    console.log("The " + this.type + " rabbit dances a jig.");
}
killerRabbit.dance(); // The killer rabbit dances a jig.
複製程式碼

回顧第4章中的示例:

var map = {};
function storePhi(event,phi){
    map[event] = phi;
}
storePhi("pizza",0.069);
storePhi("touched tree",-0.081);
複製程式碼

我們可以使用for/in迴圈遍歷物件中所有的phi係數,並使用in操作符測試物件是否包含對應的屬性。但不幸的是,這種方式會到物件的原型中尋找屬性。

Object.prototype.nonsense = "hi";
for(var name in map){
    console.log(name);
}
// pizza
// touched tree
// nonsense

console.log("nonsense" in map); // true
console.log("toString" in map); // true
複製程式碼

toString並沒有出現在for/in迴圈中,而使用in運算子測試時則返回true。這是因為JavaScript會區分“可列舉(enumerable)”與“不可列舉(nonenumerable)”屬性。

我們建立並賦予物件的所有屬性都是可列舉的。而Object.prototype中的標準屬性都不可列舉,因此這些標準屬性不會出現在for/in迴圈中。

我們可以使用Object.defineProperty函式定義自己的不可列舉屬性。

Object.defineProperty(Object.prototype,"hiddenNonsense",{
enumerable: false,
value: "hi"
})

for(var name in map){
    console.log(name); 
}
// pizza
// touched tree
console.log(map.hiddenNonsense); // hi
複製程式碼

常規的in運算子會認為Object.prototype中的屬性存在於我們的物件中。而物件的hasOwnProperty方法會告知我們物件自身是否包含某個屬性,而不會搜尋其原型。

console.log(map.hasOwnProperty("toString")); // false
複製程式碼

當你擔心某些人(裝載到你程式中的某些其他程式碼)會干擾基礎物件的原型時,我建議這樣使用for/in迴圈:

for(var name in map){
    if(map.hasOwnProperty(name)){
        // ... this is an own property
    }
}
複製程式碼

6.7 無原型物件

我們可以使用Object.create函式並根據特定原型來建立物件。你可以傳遞null作為原型,並建立一個無原型物件。

var map = Object.create(null);
map["pizza"] = 0.069;
console.log("toString" in map); // false
console.log("pizza" in map); // true
複製程式碼

6.8 多型

當編寫一段程式碼時,我們可以使用包含特定介面的物件進行工作。在這個例子中是toString方法,只要物件支援這些介面,我們就可以將這些物件插入程式碼中,並保證程式碼正常工作。

我們將這種技術稱為多型(polymorphism)。雖然在整個過程中沒有修改任何東西的形狀,但我們還是這麼稱呼這種技術。我們可以利用多型來操作不同型別的值,只要這些值支援所需的介面即可。

6.9 繪製表格

我們來通過一個稍微複雜的例子來深入瞭解一下多型和麵向物件的程式設計思想。我們編寫一個程式,將一個由表格單元格組成的二維陣列轉化成字串,該字串包含了與二維陣列對應且佈局規整的表格,要求每一列筆直整齊,每一行也要保證對齊。

首先,程式會計算每列的最小寬度和每行的最大高度,並儲存到陣列中。變數rows是一個二維陣列,其中的每個陣列元素用來表示一個單元格組成的行。

function rowHeights(rows){
    return rows.map(function(row){
        return row.reduce(function(max,cell){
            return Math.max(max,cell.minHeight());
        },0);
    });
}

function colWidths(rows){
    return rows[0].map(function(_,i){
        return rows.reduce(function(max,row){
            return Math.max(max,row[i].minWidth());
        },0);
    });
}
複製程式碼

下面是繪製表格的程式碼:

function drawTable(rows) {
    var heights = rowHeights(rows);
    var widths = colWidths(rows);
    
    function drawLine(blocks,lineNo){
        return blocks.map(function (block){
            return block[lineNo]
        }).join(" ");
    }
    
    function drawRow(row,rowNum) {
        var blocks = row.map(function(_,lineNo){
            return drawLine(blocks,lineNo);
        }).join("\n");
    }
    return rows.map(drawRow).join("\n");
}
複製程式碼

現在,我們來編寫用於建立文字單元格的建構函式,實現表格的單元格介面。

function repeat(string,times){
    var result = "";
    for(var i = 0; i < times; i++){
        result += string;
    }
    return result;
}
function TextCell(text){
    this.text = text.split("\n");
}

TextCell.prototype.minWidth = function (){
    return this.text.reduce(function (width,line){
        return Math.max(width,line.length);
    },0)
}

TextCell.prototype.minHeight = function (){
    return this.text.length;
}

TextCell.prototype.draw = function(width,height){
    var result = [];
    for(var i = 0;i < height;i++){
        var line = this.text[i] || "";
        result.push(line + repeat(" ",width - line.height));
    }
    return result;
}
複製程式碼

讓我們來使用編寫好的程式來建立一個5*5的棋盤。

var rows = [];
for(var i = 0; i < 5; i++){
    var row = [];
    for(var j = 0; j < 5;j++){
        if((j + i) % 2 == 0){
            row.push(new TextCell("##"));
        }else{
            row.push(new TextCell(" "));
        }
    }
    rows.push(row);
}

console.log(drawTable(rows));
複製程式碼

Getter與Setter

在物件中,get或set方法用於指定屬性的讀取函式和修改函式,讀取或修改屬性時會自動呼叫這些函式。

var pile = {
    elements: ["eggshell","orange peel","worm"],
    get height(){
        return this.elements.length;
    },
    set height(value){
        console.log("Ignoring attempt to set height to",value");
    }
}

console.log(pile.height); // 3
pile.height = 100; // Ignoring attempt to set height to",value
複製程式碼

6.11 繼承

每天不定時更新~,歡迎批評指正~

相關文章