《Eloquent JavaScript 3rd》筆記

icyhat發表於2018-11-14

前言篇

計算機是人思想的一部分,邏輯、秩序、規則…… 不常見的任務需要額外的程式設計才能解決。

機器很死板,無趣。

忍受機器後能享受到便利。

借鑑了自然界以及人類的模式到程式設計的設計上。

程式設計的規則原理語法很簡單,但創造出滿足複雜世界的需求會很難。

學習中的有些困難和痛苦勢必要的。這樣之後的學習就更容易了。

不要妄自菲薄,換換方式,或者歇一歇,然後堅持學習。

計算機結構是龐大的,找到圍繞目標的所需的知識,理解執行原理。

計算機很蠢,但是優點是很快。

控制計算機解決問題,就是掌控複雜度的藝術,掌控不好就崩了。。

新的問題往往用舊的眼光和方式解決不好,或者不夠好。

不斷的實踐出錯優化才能融會貫通各種情況下的考慮,而簡單的閱讀卻沒辦法做到。

語言怎麼起作用的

從最底層到最接近問題,程式語言存在著不同層面的抽象,有時候細節需要忽略,有時候又要操縱細節,這方面做得最好的是C++,而JS是主要面向瀏覽器的。它的很多抽象層級都面向瀏覽器的應用而不是效能,底層,等等。

幹同一件事的程式碼可以寫成很多樣子,是更易閱讀,更省效能還是寫程式碼用的時間最少(這是人和公司最寶貴的東西,除此之外才是金錢花費),取決於你的需求進行平衡取捨。

什麼JS

JS最早是寫瀏覽器互動、特效之類的小指令碼。

JS與Java有關,不然為啥不起別名,但僅僅是為了蹭市場名聲,技術實現上上大抵都無關。

JS在程式語言中很爛,但它面向新手,使用的人多了,一次次版本更新也變好起來了。曲線救國,人多才是王道。

JS歷史不談,2015年後ES6版本,每年都有更新,所以快更新瀏覽器與時俱進吧!

JS還在MongoDB中可以作為指令碼查詢語言、Node.js伺服器語言。

編碼

不僅要學,還要寫出來。這是一門工程學科,實現是根本,學是為了實現的更好。 Don’t assume you understand them until you’ve actually written a working solution.

全域性概覽

語言,瀏覽器,Node.js。也就是能幹什麼,在哪裡幹,還可以在哪裡幹。

值,型別,操作符

程式的表面之下是龐大的程式碼支撐著

計算機中只有資料,資料的資訊可以被解釋為控制指令、檔案內容等。

二進位制的bit,binary digit,只有0和1的序列,代表著各種資訊。

例如數字13的二進位制表示。

   0   0   0   0   1   1   0   1
 128  64  32  16   8   4   2   1
複製程式碼

易失性記憶體:8g -> 687 1947 6736 bits,687億個位元(嚇了一跳吧~)

管理如此大量的位元們,需要進行分模組處理,不然會迷失混亂。values, 程式語言中的各種值來劃分不同含義的位元資訊,值多了抽象出型別,具有同樣的特點,細化下去就是值的不同,再向上抽象就是型別的不同。

計算機是複雜的,記憶體也是複雜的,但人的生命是有限的,沒辦法瞭解所有的細節。(這是所有矛盾的源頭)

  • 但在程式語言中只關心重要的抽象,省略掉不關心的部分。
  • 比如我需要一個變數來放今天早餐花費了多少錢
    • 關心的部分:
      • 一個是名字花費cost
      • 一個是金額6
      • 合起來就是let cost = 6;
    • 不關心的部分:
      • 上面那行命令怎麼從鍵盤到CPU傳遞
      • CPU怎麼理解6cost繫結的值,還是這行命令執行6次。
      • CPU把6給我存在哪個地方
      • 這個6可以共享給其他變數,還是不能重複利用的的獨一無二的
      • cost怎麼和6進行的繫結……
    • 事實上是規定不能關心麼,不,你可以無聊或者感興趣研究它到底在某一步幹了什麼,畢竟種族中隨機的變異導致人多了就會有小部分人這麼執著於每個地方,關鍵是大部分人的時間都很寶貴,我們在時時刻刻變老,人腦處理的資訊是固定的,而計算機的發展是無數前輩智慧的結晶,我們能用一個人的一生去了解這麼龐大的知識?不存在的,只能挑重點。

數字

number, 64bits

  • 不同資訊的表示共有2^64種,每建立一個數字的變數,計算機會申請這麼大的空間,換成十進位制有20位,一般是不用關心會超過申請空間(overflow)。
  • 3.14
    • 浮點數表示只會把小數的字面值精確到某位儲存起來,不會完全復原小數,所以判斷時和標準比較差值,
    • 如我想要一個數是3.14誤差在0.01,那麼判斷if((x-3.14)<0.01), 而不是 if(x==3.14)
  • 314 可以表示為3.14e2, e(exponent)

數字一般還要分出一位用來表示正負,如果還要表示小數就要有更多位的損耗,有各種表示小數的方法,你看想要完成更多的功能,就肯定要付出一些東西。

算術

數字的主要功能是用來算術,所以這些東西不是被憑空發明出來的,而是實實在在有用才會出現在JavaScript中。

  • 二元操作符:+ - * / %

  • precedence: * / % > + -

  • %: remainder operator

    • 除餘,咔嚓咔嚓除完剩下的,多好理解,比死背強記簡單多了
  • 改變預設優先順序:括號

3種特殊數字

  1. Infinity, -Infinity
  2. NaN: not a number

字串

  • 好寬容,三種符號可表示字串,沒有character型別
    1. backticks,反引號中的string,也叫template literal
    2. 換行符不用進行轉義就可以保留
    3. half of 100 is ${100 / 2}
    4. 計算${}
    5. 轉換成字元
    6. 被包含
    7. ''
    8. ""
  • 轉義字元:backslash,\
    • 意味著這個字元之後的應該特殊對待,
  • 字串中的每個字母佔16bits,小於Unicode數量,所以有些字元用佔2個字母的空
  • 只有+操作符:concatenates

一元操作符

操作符除了用符號表示,再多了符號不夠用命名關鍵字表示.

操作符的運算元可以是一個,兩個……

一元操作符:

  • typeof: return a string of type of the operand

符號的一詞多義:

  • 原因:因為鍵盤的符號有限不夠用,有的符號又當爹又當媽
  • -(2-1):
    • 第一個-,unary operator,操作的物件是一個,即2-1的結果,語義是將一個數置負,
    • 第二個-,binary operator,需要兩個操作物件,語義是有兩個數,他們想減

布林值

資訊的儲存最理想狀態是e次方,即2.718,而現實離理想還有段距離,所以目前的計算機是基於二進位制的,實際上三進位制更理想。不過二進位制是第二好的選擇,比十進位制不知道高到哪裡去了。

既然是二進位制,就免不了這個二的狀態由哪兩個表示,計算機說是0和1,實際上在不同器件裡面的表示,有用電平高低的,有用正弦不同的,程式利用truefalse,平常我們說話用有和沒有,是或者不是,開或者關,如果是三進位制那還包括一種情況,那就是不知

比較

比較的結果是個布林值

字元的比較是按照ASCII碼以及Unicode碼來的,而不是字典中從A到Z的順序,注意Unicode字元是ASCII的超集,然後ASCII中的編碼在Unicode中大小是相同的,所謂的相容ASCII,無非是前面多8個0。

只有一種值不等於它自己: NaN,它的含義就是用來表示無意義的結果,所以他也不和其他無意義結果相等。Oh,shit,難以理解。。

邏輯操作符

語義:注意它的操作物件是布林值,不是用來算數的,儘管數字0和1會被變數提升轉換成true 和 false。

  • and: &&
  • or: ||
  • not: !

precedence: > == > && > ||

一個三元的邏輯操作符:true ? 1 : 2, ternary, conditional operator, question mark and a colon

三元操作符短路,意味著有些語句不執行。

Short-circuiting of logical operators

短路,左邊算完了如果返回左邊,右邊就不驗證了,稱short-circuit evaluation.

||左邊true就返回左邊,false就返回右邊。 &&左邊false就返回左邊,true就返回右邊。

我從未聽說過布林邏輯的處理是這樣的,什麼叫and運算?左邊false,就返回左邊物件?為什麼不是返回false?我要你這個運算何用?這種神奇的功能我自己八輩子都用不到

垃圾語言,奇葩設定,毀我青春,****。

WTF為什麼不和C++,Java一樣?

Examples of expressions that can be converted to false are:

null;
NaN;
0;
empty string ("" or '' or ``); 
undefined.
複製程式碼
  1. ||先把左邊轉換成布林值,是個數就返回左邊,不是個數返回右邊
  2. 轉化規則
-  `0`, `NaN`, `""`, `null`, `undifined`會被算作 `false`
- 其他的所有算作`true`
複製程式碼
  1. true 則返回左邊
- `console.log('bat||'ant')  // -> bat`
複製程式碼
  1. false 則返回右邊
- `console.log(null||'hi')  // -> hi`
複製程式碼
  • 利用這個特性,可以將可能是空值的變數加上||,使之成為備胎。

空值

設計上的委曲求全,唉,可是Rust語言不火啊,人們並不需要正確,人們需要相容穩定能用。

  1. null
  2. undefined

自動型別轉換

JS總是喜歡能處理你給的各種值,也就是對你很包容,但這對於精確的控制卻不好。語言的設計

type coercion

  • null*8 -> 0

  • "5" - 1 -> 4字元拼接優先於數值計算。

  • "5" + 1 -> 51

  • NaN如果一旦產生,那麼與此相關的結果還是會NaN

  • 自動型別轉換和嚴格相等

    • ==!=
      • same type
        • 除了NaN
      • diff type
        • nullundefined之間為true,和其他為false
        • 其他型別自動轉換
    • ===!==
      • 不包括自動型別轉換

程式結構

表示式和語句 expressions and statements

磚塊在高樓中才能體現更大的價值。意思是良禽擇木而棲才能發揮更大價值?

能夠產生一個值的程式碼塊稱之為表示式,expression。字面值,帶括號,運算式都是expression

expression之間互相組合、巢狀組成了語義更復雜的expression。

expression是某一小段子句,而語句,statement是一句完整的話。程式就是一列語句們的集合。

最簡單的statement是一個expression加上一個分號。(可忽略的分號。。。)

expression產生一個值,然後被周圍的程式碼所利用。

而statement隻立足於他自己,除非指定和其他東西有交集。當改變了螢幕上的文字或者改變了計算機內部的一些狀態,以致於影響後來的語句執行的結果,這就叫做影響,effects。像1;這種語句確實改變了了計算機內部的東西,但對於別的程式碼沒有明顯的作用。

分號這個東西大部分時間可加可不加,但是你懂的總有意外。有時候不加分號會讓會讓兩行程式碼變成互相影響的一句statement,為了安全、不找麻煩,還是加上吧,不然要認識很多哪些是必須加分號的複雜情況。

繫結 bindings

表示式會產生一個值,比如1+2,但是產生的結果3,如果不立刻使用它,或找一個空間分給他,它馬上就不見了。

let caught = 5 * 5;

variable or binding

keyword: let, 宣告+賦值

  • 宣告瞭caught,就是宣佈要有光!,然後計算機記憶體裡面就找到一個地方對應著這個caught.
  • 賦值,右邊5*5的結果繫結到了caught上。
  • 只宣告沒賦值的情況為undifined
  • 之後再次出現caught,它就會被要麼作為空間地址,要麼取它的值25
  • 一次聲名,多次繫結。
  • 繫結更像觸手一樣可以多個繫結指向同一個值。這句話很費解,是指變數只是個引用,這個值是共享的?還是說繫結的值和引用的位置是分離的?

var是個歷史遺留問題,const一次宣告賦值為常量,鞠躬盡瘁,死而後已。

Binding names

字母、數字、$_,不能以數字開頭,不能用關鍵字,不能用保留字。

其實就一個原則就好了,字母開頭,各種駝峰代表變數、函式、還是類。也別起什麼各種奇葩名字,沒事找事。

Environment

程式啟動的時候,環境就啟用了語言本身的一部分繫結,以及提供互動的一些繫結。

Functions

預設環境提供的一些變數型別為function.

呼叫、請求函式的執行,稱invoking, calling, applying.

回想數學f(x)=y+1;函式要有輸入x,用括號來表示。稱arguments, 這個引數可能(x,y),也可以是(x,3,4)

console.log function

console.log 不是一個繫結,它包含了一個句號,對吧,變數名是不允許的,那它是什麼呢? console是一個繫結,.log代表檢索這個繫結的一個名為log的property。

Return values

side effect: 函式的主要作用是用來返回一個值的,那麼顯示一段文字,彈出對話方塊叫做副作用。(這是函式定義,不是寫程式碼的目的。。)

由於函式要return一個結果,所以它符合一個expression,也就是可以和另外的expression組合巢狀在一句statement中。console.log(Math.min(1, 3)+1);

Control flow

straight-line

prompt中輸入的值為string,用Number()轉換成number進行計算,其實不用人工轉化下一行計算的時候會自動轉換型別的。。

conditional execution

原先一條路走到黑,現在變成二選一,然後繼續走下面的語句。

if (1 + 1 == 2) console.log("It's true");

一般情況下程式碼要寫的讓人看著方便,大部分都要加大括號,除非一行簡單的if。

多重巢狀的時候每次當下判斷都是二選一,注意合併簡化

while & do loop

2^10 (2 to the 10th power)

do while,for 和 while(){}的區別就是語義上的

  • 最少幹一次用do while
  • 先判斷條件再做就用while
  • for和wihle大部分等價
    • 將三個東西作為一個整體劃分比while強一點。哪三個東西呢?第一個初始化條件就是while之前的語句,第二個判斷條件就是while的判斷條件,最後一個執行語句可以放在while執行語句的最後。
    • break時都是直接退出
    • 小部分的區別在於continue
      • while會直接跳過後面程式碼重新判斷,
      • for會直接跳過後面的程式碼,執行第三條語句後再去重新判斷。

縮排空格、換行

純粹為了易讀性,一個還是兩個空格、一個還是多個換行都不影響它的邏輯。但換不換行是有區別的,參考加不加;的各種規則。

for迴圈

明明有while為什麼還要發明for呢?因為常用啊,因為程式碼邏輯驚人的相似重複的部分人就願意把它封裝成新東西,輕輕一揮,魔法就實現了,多好。

for和while的區別就是while經常需要一個迴圈計數器,並且每輪迴圈都必須要改動這個迴圈計數器,再加上本來的條件判斷,這不就是for啦?

break continue

for中不設迴圈終止判斷,就可以把判斷挪到迴圈體中if(){*; *; break;}中,這樣的好處是判斷完之後可以繼續在for的環境中執行一些語句。也就是說如果有if(){break;},考慮是否可以放在for語句頭中?

break 如果用原來的機制實現,相當於在原始碼前加了個if,並且新增的一個分支只有一個改變迴圈條件、並且還要抵消for第三個語句(如果在for語句中)。你看如今你想要的,一個break就可以解決了。

continue代表著跳過這句之後的迴圈體中的程式碼,執行for第三部分(如果在for語句中)。然後繼續下一次迴圈

continue 如果用原來機制實現,相當於原始碼前新增if,並且新增的一個分支什麼都不做,並且還要抵消for第三個語句(如果在for語句中)。

這兩個一個是直接跳出所有迴圈,一個跳出當前這一輪迴圈。

updating bindings succinctly

當一個變數的變化是由原來的自己進行更新的話,可以直接在賦值的時候指定什麼操作

counter = counter +1;

// 更簡便的自我更新

counter += 1;

// 對於自我的加一減一,還可以縮寫

counter ++;

// 在Java中 a++ 和 ++a 實現不一樣,返回的物件是一份原a物件的拷貝物件,所以值和原a一樣,後者返回的是加完的a物件,所以相對於原a值加一。在JS中還不知道啥實現

複製程式碼

switch

注意default:break:,雖然很多時候工具會自動猜測你的意圖,幫你補全一些不嚴謹的邏輯。

一個多狀態的值 --> 多重if的程式碼的簡寫模式 --> 就是switch。。常用的東西才會被髮明。

多對多的怎麼辦呢?用函式封裝一下,多個輸入值,每個值有多個狀態,隨你所願的組合判斷,

Capitalization

  1. 全小寫不易讀
  2. 下劃線,打字多太累
  3. 全大寫是函式繫結的構造器
  4. 駝峰風格是慣例。。

一切都是有原因的,但是所有的事我們真的需要知道原因?

註釋

有時候程式碼並不能層次鮮明的由淺入深,由全域性到定位區域性資訊的導航功能,這個時候需要註釋來快速定位一大坨只有一個名字的程式碼到底是幹什麼的。

VS Code中快捷鍵Ctrl /可以快速判斷當前是HTML還是CSS還是JavaScript還是JSX程式碼進行插入相應的註釋。

注意多行巢狀沒有實現(不是不能實現)交錯的處理。以及 //右邊全算作註釋

練習

Looping a triangle

Q1:

#
##
###
####
#####
######
#######
複製程式碼

A1:

for (let i=0, j=""; i<7; i++) {
  j += "#";
  console.log(j);
}
複製程式碼
  • i僅代表從第1到第7行,而這每一行列印出幾個的井號跟i無關(至少在這題裡面,有的題列印的星星數量可能跟行號有數學關係),這7行每行列印出什麼跟j的自加有關。
  • 當然這兩行程式碼因為每次迭代都執行了,可以跟i++放在一起,看個人喜好了,放在那裡太擠太醜了不是麼。。
  • 僅從該題目的話,這些程式碼是合格的,但如果考慮擴充套件性(比如使用者指定打多少行、每行的#的數量表示式),效率(是一次性列印,還是逐行列印)等等,就會有不同的解法

Q2:

print number 1-100, 3的倍數用Fizz代替,5的倍數用Buzz代替,若是3和5的倍數,用15代替
複製程式碼

A2:

for(let i=1; i<=100; i++) {
  let result = new Array();
  let string = "";
  let number = i;
  if(i%3===0) {
    string += "Fizz";
  }
  if(i%5===0) {
    string += "Buzz";
  }
  result.push(string||number);
  for(let i of result) {
    console.log(i);
  }
}
複製程式碼
  • if 沒有else的時候,要注意else和if之後的語句不一樣,因為else是做過判斷的,跟判斷有關就加個else,跟判斷無關,就不加else,直接在後面寫。
  • i代表著迭代多少次,number其實可以直接用i不用新建,因為正好題目是1到100,但如果是100到200呢?這就需要將列印的次數和列印的數字區分開,雖然有時候他們值相等不用新建變數,但他們語義是不一樣的!
  • 首先3和5的倍數是兩個原子不可拆的操作,而15不是,所以再新增分支判斷15就很浪費,而15實際上就是3和5字元的相加,所以每一步字元是加操作。其次根據JavaScrit||的奇葩設定正好幫助返回FizzBuzz或者備胎數字,納愛斯。
  • 將所有結果的資料存到了陣列中,不僅合乎列印出的結果,更容易日後其他操作,雖然只是一道題,不需要啥擴充套件性 Q3:
Chessboard
 # # # #
# # # # 
 # # # #
# # # # 
 # # # #
# # # # 
 # # # #
# # # #
複製程式碼

A3:

// 若將該功能封裝為函式,下面就是輸入的引數變數,n階矩陣,兩種要列印的符號
  let size = 8;
  let symbol1 = " ";
  let symbol2 = "#";
// 橫向重複的最小單元,要進行O(logn) 替換掉 O(n)的重複字元的複製
  let symbolSum = symbol1 + symbol2;
  let contentOddAddon = size%2==1?symbol1:"";
  let contentOdd = repeatCharBinary(symbolSum, Math.floor(size/2)) + contentOddAddon;
// 求偶數行的內容,由奇數行內容轉換,踢掉第一個,再根據最後一個新增適當元素
  let contentEvenArray = contentOdd.split("");
  contentEvenArray.shift();
  contentEvenArray.push((contentEvenArray[contentEvenArray.length-1]==symbol1)?symbol2:symbol1);
  let contentEven = contentEvenArray.join("");
// 縱向重複的最小單元,要進行O(logn) 替換掉 O(n)的重複字元的複製
  let contentTwoLine = contentOdd + "\n" + contentEven + "\n";
  let contentOddLineAddon = size%2===1?contentOdd:"";
  let content = repeatCharBinary(contentTwoLine, Math.floor(size/2)) + contentOddLineAddon;
  console.log(content);
// 重複某個字元的nlog函式
  function repeatCharBinary(char, n) {
    // 這個地方需要進行重複計算字元,可以將程式碼封裝為一個函式
    // 線性複製char更改為指數級複製
    // 將十進位制n轉換為二進位制,除2取餘,對應1、2、4、8、16的權重
    // 餘1則存在,那麼將結果加上對應的權重數量的字串,餘0則表示不存在,那就不加這位的字串。
    let tmp="";
    while(n!=0) {
      if(n%2==1) {
        tmp += char;
      }
      n = parseInt(n/2); //結果從低位算起,餘1則存在,結果加之,繼續除2餘1,算更高一位
      // 之前算每一位的權重是否存在,這一行是算如果存在,對應的字串長多少,每輪要變為更低位字串的兩倍。
      char += char; 
    }
    return tmp;
  }
複製程式碼
  • 題目兩種不同的符號,交叉成字串,寬n,高n,奇偶互異,思路是
  1. 計算出第一行的內容
  2. 模式就是兩個字元的多次重複
  3. 偶數n/2次
  4. 基數n/2次+第一個字元
  5. 這麼多次的重複採用8421的指數翻倍相加?
  6. 第一行內容佇列一進一出形成第二行內容
  7. 第一二行作為一個重複的最小單位,然後多次指數重複輸出
  8. 取整要用Math.floor(),預設的取整不知實現機制,不過總是少一個,很奇怪。

Function

搞計算機的是天才麼?不是的,大家只是站在巨人的肩膀上,用別人生產的磚塊壘起自己想要的一面牆。

函式是個很重要的內容。它將一大塊程式碼濃縮成一個名字。這樣可以分解一個大問題的龐大程式碼,變成幾個小問題的程式碼,只抽象出變化的部分,稱之為輸入,然後輸入引數進入同樣邏輯的程式碼中,產生不同的輸出。而僅僅只需要用一個函式名字就可以關聯變數和邏輯。

優秀的文章經常通過不同的詞彙乃至精妙絕倫的句子來體現文章的藝術,而程式設計不同,它只在乎能幹成什麼事,所以可能不那麼動聽引人入勝。

程式設計中的語言就像公式一樣枯燥,但如果懂了就明白它的精確和簡潔,不同於文章給人精神上的享受,類似的賬單也很枯燥,但它精確簡潔,並且你需要它的時候,他能滿足你。

defining a function

let area = function(x,y) { return x*y;}
複製程式碼

可以通過繫結到一個變數來重複呼叫這個函式。

函式功能

  1. 輸入
  2. 可以沒有
  3. 輸出
  4. 沒有return,執行完預設return 一個undefined
  5. return
  6. return; 則return 一個undefined
  7. side effect

bindings and scopes

作用域只在當前以及子作用域

  1. Global
  2. Local bindings
  3. 函式的每次呼叫都會新建一個例項
  4. ES2015前,只有函式能建立新作用域(比如if for 都預設是global)
  5. ES2015後,JS的作用域終於和C++Java等正常語言一致了。。

nested scope

多級巢狀,lexical scoping

Functions as values

let hi = function() {

};
複製程式碼

新建變數的函式繫結,要加分號。

函式繫結的名字呢,還可以被繫結為別的東西,所以變數並不是函式,他只是個指示器,代表著它可能指向一個數字、字串、或者是函式等。

declaration notation

宣告式的函式標記, 僅僅宣告,不進行呼叫

function hello() {}
複製程式碼
  1. 可以不用加分號
  2. 這種函式在執行的時候會挪到當前作用域的最開始部分優先於其他程式碼

arrow function

let hey = (x, y) => {

};
複製程式碼

省略了函式名,然後直接將輸入指向輸出。

const square = x => x*x;
複製程式碼
  1. 只有1個變數的時候可以省略括號
  2. 只有一行語句要return的話,可以省略大括號。 涉及到知識點
  3. 遞迴
  • 要有終止條件
  • 為了學習語法的demo並不代表最優解
  • 既然出現遞迴和迴圈都可以乾的事, 那麼說明他們有擅長的方面, 不然沒價值的東西不會出現在語言中,但是對於某個問題可能只有其中一種是最好的.
  1. 邊界條件, 特殊值的覆蓋
  • 關鍵要找到典型的例子進行覆蓋測試.
  • 如果例子不用典型代表, 測試的次數太多.
  • 如果例子選出來了但代表不典型, 覆蓋的範圍太少.
  1. 有問題先Google
  • 那裡是廣闊的海洋, 數不清的金銀混雜著狗屎, 但一般而言被篩選到第一頁搜尋結果的都是上好的金子.
  1. 預設引數
  2. 箭頭函式
  3. for中的i,預設為迴圈計數器
  • 如果用它做了別的含義,最好新建變數,使之修改的時候更容易
  1. 統一的函式結果的返回介面, 容易修改和閱讀
  • 但在多次遞迴中直接不管進入到哪個分支中都進行return, 是不是語義更清晰些?少繞一步賦值給結果,再return出去.
  • 但是如果是複雜的大量程式碼有利於定位return值的變化
  1. 拼寫錯誤, WTF!
  2. 再小的程式碼, 實現起來也是要費些周折, 不要忽略實際問題而想當然
  3. 語法是會慢慢進化幫助開發者的, 比如最後一個引數之後也可以加,, 方便日後直接新增.
let power = (base, exponent=2) => {
  let result;
  if(exponent===0) {
    result = 1;
  } else {
    result = base * power(base, exponent-1);
  }
  return result;
};
console.log(
  power(0),
  power(1),
  power(0,3),
  power(2,1),
  power(2,2),
  power(3),
  power(100,3),
  power(100,100),
);
複製程式碼

the call stack

函式執行完會把return的值返還到呼叫它的地方,然後繼續執行接下來的程式碼。

call stack: 函式呼叫的棧

  1. 每呼叫一個新的就會push進一個新的函式執行完再pop出去
  2. 這個棧是佔記憶體空間的,如果呼叫、遞迴的函式太多,棧會溢位

optional arguments

給一個函式多個引數,而函式只要一個的時候,它就取一個並且繼續執行。

而如果給的函式不夠,那麼預設undefined

怎麼樣JS是不是很混蛋?如果程式設計師無意寫錯了,他都不報錯?

那如果有意寫成這樣,相當於C++ 中的函式過載。

function minus(a, b) {
  if (b === undefined) return -a;
  else return a - b;
}
複製程式碼

哇,好牛逼。。這樣竟然實現了減法。。一個數就是減自己,兩個就是互相減

預設引數, 另外指定了則按指定的,無指定的按預設的

funcition area(redius, PI=3.14) {

}
複製程式碼

closure

ES2015後改為了正常程式語言的作用域規則。chain scope,子可以看到父,父不可看到子的變數。 ES2015之前只有function有自己的作用域。

ES2015之前定義的var關鍵字最好用let, const代替。 如果沒有關鍵字直接hi="hi",預設是全域性作用域

內部作用域可以呼叫父作用域的變數,那麼如果父作用域想呼叫子作用域呢?

讓父函式新建一個子函式,並return這個子函式,子函式可以訪問它爹,然後新建一個變數承接父函式,這個變數就繫結到了return回的子函式。

如果這個變數是全域性的,那麼這個子函式不會回收,所以它爹也不會回收。

同樣不用var新建的變數如果繫結到了一個函式,也是全域性變數,不會回收。

所以注意垃圾滿了。儘量不需要不要用閉包。

這樣引用一個函式例項的一個子函式,稱之為一個閉包。

recursion

函式呼叫自己叫做遞迴,遞迴要有終止條件return出去.

  1. 遞迴實現比for 更慢3倍。。因為呼叫函式需要的資源更多。
  2. 注意棧溢位

優雅和速度總是很有趣的矛盾體。。。更快意味著可能只顧目的,吃相不好看。更好看,可能顧及的就多,妨礙了核心目標。所以以切都是權衡,哪些必須要快不顧長相,快到什麼程度?同樣相反。

一個程式的專案實現的地方有很多,市場、宣傳、技術、組織。而技術之中也有開發效率、機器效率、使用者友好、利於維護等一堆互相矛盾的點,要抓住主要的目的,勢必就要在其他點上妥協。你不可能用最快的開發速度,寫出最省機器資源、最利於維護擴充套件、最對使用者友好的程式碼。

機器的快速發展,所以程式語言越來越高階,庫越來越順手,你如果老是擔心這個地方效能不好,那麼你想想你有沒有因為自由的呼吸空氣而覺得浪費,有沒有覺得自己跑步呼吸了更多的空氣而浪費?為什麼?因為空氣與你的呼吸相比很廉價,同樣在機器相對於人的時間很廉價。所以那段程式碼利於你快速理解閱讀、並且最快的滿足功能,為什麼要去省下幾口空氣呢?除非你掉進了水裡(機器執行吃力),當空氣(效能)真的很重要的時候,再去考慮空氣(效能)。

甚至很多你覺的一定要優化的東西根本不值一提, 就像你多呼吸了兩口空氣一樣無關緊要.你收貨的只有沾沾自喜以為賺到了, 但你失去的是你最寶貴的生命的幾分鐘甚至幾個小時.

還有很多時候你所謂的優化在優化編譯器的專業大師前不值一提, 它甚至本來可以大幅度自動優化的地方,被你用生澀的奇技淫巧僅僅優化了一點.不要自作聰明, 衡量你生命的付出和換來的產出而不僅僅是沾沾自喜.

求從1開始, [+5]或者[*3]得到指定數的方法

遞迴在多分支的時候實現起來比迴圈更好。

  1. 注意遞迴終結條件, 只能是成功或者找不到, 而不是最短路徑.
  2. 本質就是暴力嘗試...只不過用遞迴寫起來簡單.
  3. 只有反引號裡才能透明寫string模板, ${ variable }新增變數.
  4. 兩個引數一個作為計算, 一個作為輸出展示. 這是兩個資訊, 畢竟沒辦法列印一個算式的過程? 目前孤陋寡聞...
  5. 運用遞迴要找到問題中能轉化成初始條件, 層層相扣重複的邏輯, 以及終結條件, 不要用過程來思考, 容易卡死..
    let findIt = target => {
      function find(current, history) {
        let result;
        if (current === target) {
          result = history;
        } else if (current > target) {
          result = null;
        } else {
          result =  find(current + 5, `(${history} + 5)`) || 
                    find(current * 3, `(${history} * 3)`);
        }
        return result;
      }
      return find(1, "1");
    }
複製程式碼

growing functions

我的函式, 進化吧! 一般對函式的需求體現在兩方面:

  1. 重複的程式碼段,很大程度上邏輯相似.如果不用函式的隱患有:
  2. 最重要的不是效能, 而是多次重複的程式碼更大可能的出錯.
  3. 其次是程式碼太多不利於人閱讀.
  4. 再然後才會是機器效能的浪費.
  5. 你覺的那應該有這麼一個函式,儘管還沒寫,預先的感覺.

農場第一版

  1. 007 Cows
  2. 011 Chickens

即保證數字為3, 真的是聞所未聞, 用字串長度來做判斷讓其一次次的加0直到3位...

function printFarmInventory(cows, chickens) {
  let cowString = String(cows);
  while (cowString.length < 3) {
    cowString = "0" + cowString;
  }
  console.log(`${cowString} Cows`);
  let chickenString = String(chickens);
  while (chickenString.length < 3) {
    chickenString = "0" + chickenString;
  }
  console.log(`${chickenString} Chickens`);
}
printFarmInventory(7, 11);
複製程式碼

農場第二版

當我們在農場又加了豬之後, 重複的程式碼塊被封裝再一個函式裡面.

function printZeroPaddedWithLabel(number, label) {
  let 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);
複製程式碼

函式名字又臭又長, 這裡面有幾個基本的資訊

  • 數字補全到多少位(width), 需要補全(width-length)位, 可能農場變大之後就上千上萬需要更多位
  • 數字本身是多少(number), 它的範圍如果超過寬度呢呢?
  • 用什麼補全會變化麼? 不會..因為是用0, 這叫封裝不變的, 留出變化的介面(函式的輸入)

一個名字意思精煉小巧的函式, 不僅能讓人一眼看出它的意義, 還可以讓別的程式碼複用這種基礎的函式, 想一想語言本身帶的那些常用的函式, 越是通用越是功能單一, 越是可以互相組合成為更強大的功能. 想一想鍵盤上的26個字元, 不僅能打出成千上萬的英文文章, 還能打出漢字來.靠的就是抽象基本模式,使得記憶負擔小, 這叫討人喜歡, 然後互相組合能打出各種字, 這叫功能強大. 如果是無意義的編碼,你要記2k多種沒有規律的基本漢字的編碼, 不討人喜歡吧, 雖然功能也強大.

原則: 不要亂優化修改, 除非你明確的需要, 不然一切從簡,快,正確. 當你僅僅需要一朵花的時候, 那就不要又設定花盆又設定肥料. 要控制住你的衝動, 你優化的東西可能和你的目的沒多大關聯, 僅僅是來自基因的傾向性要讓你熟悉探索周圍環境, 因為你可能沒寫多少有意義的程式碼, 卻浪費時間改來改去讓機器記憶體降低0.01%?那主要任務怎麼辦?

function zeroPad(number, width) {
  let 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);
複製程式碼

functions and side effects

函式可以用來return一個有用的值, 也可以side effect.

但主要作用在return的值上面的函式, 才可以有用的和其他函式組合.為啥? 因為side effect就在函式內部, 輸出的資訊沒法傳給別的函式.

pure function: 不依賴其他程式碼的side effect, 也不輸出自己的side effect. 每次同樣的輸入, 都會產生同樣的輸出. 一個純函式如果在某一個地方正常, 那麼它在所有地方都正常.而其他函式可能依賴的環境不同.

在機器朝向純數學發展的路上時, 純函式肯定擁有更嚴謹有效的解決問題的能力, 但... 機器並不是純粹的理想產物,它被現實一代代所創造優化, 所以用面向機器的程式碼在目前往往更高效, 而純函式作為高階抽象, 耗費更多的資源.

Exercise

min()函式

let min = (x,y) => x>y?y:x;
複製程式碼

recusion isEven()函式

基本:

  1. 0是偶數
  2. 1是奇數 演繹: N的奇偶性和N-2是一樣的 終止條件: n=0或n=1;
let isEven = (x) => {
  let result;
  switch(x) {
    case 0:
      result = false;
      break;
    case 1:
      result = true;
      break;
    default:
      result = isEven(x>0?x-2:x+2);
      break; 
  }
  return result;
};
console.log(isEven(50));
// → true
console.log(isEven(75));
// → false
console.log(isEven(-1));
// → ??
// 這個多次判斷正負的語句可以提前置於外部, 講這個函式封閉為閉包訪問靜態變數, 使判斷正負只執行一次.
複製程式碼

B counting

    let countChar = (string, char) => {
      let cnt = 0;
      for(let i=0; i<string.length; i++) {
        if(string[i] === char)cnt++;
      }
      return cnt;
    }
    console.log(countChar("HelHsdfklkjlkhloHi", "H"))
複製程式碼

相關文章