close

˙緣起

網路上看到保哥的課程,想想自己受益於他的「前端工程師必須學會的現代化前端開發工具」,裡面有很多很有用的資訊,覺得這課程應該可以將自己的 js 梳理一遍,就報名了 .... 就額滿了!
好家在台北場沒了還有台中場,台中場沒了還有高雄場,高雄場沒了還有.... 我反正是報到了台中就是了!

 

˙上課 Agenda

• 物件、變數與型別
• JavaScript 基礎物件概念
• JavaScript 物件導向基礎
上面是上課的議題,光看這些,會覺得是一堂新手入門的課程,但由於這堂課僅僅一天 - 有哪位在學其他程式語言的時候,也不可能一天就從變數型別到物件導向吧!?
因此與其要說是給新手入門的課程,不如說給寫了一陣子的 js 開發者們所開的課程。此時,你已經有了實務經驗,開始認識到社會的黑暗 js 奇特的部分,以及在解 bug 時諸多無法解釋的「解法」,重新從最基本的地方開始,可以將觀念梳理一遍;越了解他的底層運作方式,之後的路可以走的越遠。

 

˙Javascript 語言特性

js 是一個腳本語言(scripting language),表示在開發時沒有 compiler,有著易於開發但是執行效率低的問題。相較於此,像是 C、C++、Java 等編譯式語言(compiling language) 在開發時就會有編譯器,注重型別,完整、不易出錯且效能高,但是開發速度慢。

一個 scripting language,開發快速的原因是因為對於同一段邏輯,可以有很多種寫法來撰寫,就會產生各式各樣的 coding style,百家爭鳴、百花綻放、百 ....,這件同時也有不好的影響,就是容易寫出不好的 code,不方便閱讀、不容易擴增、不好除錯 .... 等等問題。

而現在 js 這個語言,從只能在瀏覽器中運作,如同 Java、C 只能在 OS 中運作,一直到現在因為 Node.js 的出現,開始可以在一般 os 中運作,大概就像是鯊魚長出了後腳,或是老虎長出了翅膀一般恐怖,順著這個慣性,Electron.js 以及 GPU.js 這些的開發讓 js 得以進軍桌面應用程式、硬體操作等領域,同時也不能忘記 Unity3D,這個悠游於遊戲開發界上的套件。從這個趨勢來看,之後只能是越走越廣,越走越深了。

參考:Scripting Languages 簡介How is JavaScript different from languages like C++ and Java?

 

˙call by sharing

最重要觀念:call by sharing,表示是將一個數值的位置(址)連結到變數,而非一個變數的位置(call by reference),或是一個變數的值(call by value)。實際運作起來時,跟 call by reference 有 87% 像... 所以是 13% 的不像!

var a = 1,
b = a;
b = 2;
a //1
var a = {obj: 1},
b = a;
b.obj = 2;
a //{obj: 2}
var a = {obj: 1},
b = a;
b = {obj: 2};
a //{obj: 1}

簡單看基本運作,例二中 b 值的變更,讓物件中 obj 參照到的址從 1 的位址變到 2 的址,所以 a 印出來時也跟著變化... 兩位穿的是同一件衣服嘛!而在例三中,則是先在記憶體中宣告了新的區塊、新的值,然後賦值這動作就讓 b 拿到了新的值({obj: 2})的位址,但這時候已經與 a 無關了,因此從此兩位開始各走各的。

若說比較出 call by reference 與 call by sharing 的差異,應該是在 function 的情況底下看出來:

function func(num){ num = 20; return num; } 
var result1 = 10; 
var result2 = func(result1); 
console.log(result1, result2); //10, 20
function beGirl(obj){ obj.sex = 'girl'; } 
var person = new Object(); 
beGirl(person); 
console.log(person.sex); //girl
function beBoy(obj){ obj = {sex: 'boy'}; } 
var person= {sex: 'not sure'}; 
beBoy(person); 
console.log(person.sex); //{set: 'not sure'}

從例二與例三看起來,情況變得有點詭異,明明例二中,像是把 ref 傳進 function 中,obj 的值變化,那位 person 的值也跟著變了。但在例三中,同樣的在 function 去改變值,但得到的結果卻與例二不一樣。

ECMASript 中的解釋是這樣:

The main point of this strategy is that function receives the copy of the reference to object. This reference copy is associated with the formal parameter and is its value.

Regardless the fact that the concept of the reference in this case appears, this strategy should not be treated as call by reference (though, in this case the majority makes a mistake), because the value of the argument is not the direct alias, but the copy of the address.

粗體的地方表示說,把記憶體中的位置複製給了函數中的變數。

要注意的是,在例子中,一個 js object 是一個儲存位址的位址 - 他佔有一個位址,然後將該位址傳給變數(person = {}),但同時,他也儲存著眾多變數的「值的位址」({sex: 'girl', name: 'JustinB'}),所以在值運作的判斷上,可以簡單區分成基本類型、引用類型。

基本類型像是 string、number... 等基本類別就是產生新的記憶體,將新址傳給變數。

而若是引用類型,js object、array,則區分成是否是對其中的屬性進行操作(obj.sex = xxx、arr[0] = 1),在例二中就是如此操作,在函數外顯示出變化。

另一個就是直接改變引用類型變數的址,這邊運作就像是基本類型一樣:產生新的記憶體,然後將新址傳給變數,如同例三中的情況。

眾多參考:保哥、簡單介紹JavaScript參數傳遞call by sharing——JavaScript中“共享传参”和“按值传参”的理解
Know the passing mechanismAn Illustrated Guide to Parameter Passing in JavaScriptJS是按值传递还是按引用传递?wiki:Evaluation strategy一道常被人轻视的前端JS面试题

 

˙變數 ft. 屬性 

變數的宣告,相當於是在根物件(瀏覽器中是 window)底下建立屬性:

var a = 1;
window.a = 2;
a //2

而只有用 var 宣告的才會是變數,其餘的,包含不加 var 的變數宣告都算是屬性的建立:

foo = 3; //相當於 window.foo = 3;
delete window.foo; //true
var goo = 2;
delete window.goo; //false

變數與屬性的運用方式幾乎一樣,差別是就是

1.delete 是否可以使用
2.存取一個不存在的屬性時:undefined;存取一個不存在的變數時:Uncaught ReferenceError

foo = 3;
delete window.foo; //true
foo //undefined
goo //Uncaught ReferenceError: goo is not defined

另外,吾人常常運用 if(typeof(obj.c) == 'undefined') {...} 來判斷屬性是否存在,但其實這段判斷式在邏輯上有盲點:運用的根據是「當沒有該屬性時,會回傳 undefined」,但實際上,若是已經宣告了該屬性,但該屬性卻尚未賦值,此時預設值為 undefined,或是此時該值是 undefined 時,也會通過這個判斷式。

obj = {};
obj.x = window.y; //obj = {x: undefined}
typeof(obj.x) == 'undefined'; //true

通常來說,這個判斷式是用來做「要不要給值」的根據,因此即使是在有該屬性且通過判斷式的情況下(obj.x),該值仍然是 undefined,並不會發生甚麼錯誤。

但以邏輯上來說,也許這個判斷式會是更全面一點:if ('x' in obj) {...} 

obj = {};
'x' in obj; //false
obj.x = window.y; //obj = {x: undefined}
'x' in obj; //true

 

˙Boolean

null 與 undefined 兩個比較時會產生 true,

null == null //true
null == undefined //true
undefined //true

其他時候,跟不管誰去比較都是 false

null == false //false
null == true //false

而 NaN 則跟任何人,包括自己比,都是 false

NaN == null //false
NaN == NaN //false

唯一能判斷是 NaN 的方法就是 isNaN()

isNaN(NaN) //true

完整的比對表:JavaScript-Equality-Table

該表告訴我們:「==」的情況非常複雜,但「===」就相對單純,請愛用「===」、「!==」

 

new Boolean()的使用,裡面的變數百百種,有的會是 true,有的會是 false,而只有下列 6 種值會出現false:false本身、0、''(空字串)、null、undefined、NaN

new Boolean(false);
new Boolean(0);
new Boolean('');
new Boolean(null);
new Boolean(undefined);
new Boolean(NaN);

boolean 還有像是以下的語法

function something(setting){
  myDefault = setting || 2; //表示若 setting 沒有放入值(undefined)的話,就會給予 2 當作預設值
}
function something(callback) {
  callback && callback(); 
  // 常見、有點 tricky 的寫法,相當於 if(callback) callback()
  // 利用邏輯判斷:如果第一個為 false,則結果為第一個,否則就是第二個
}

 

˙Number 

NaN 無法做計算,但依舊是個數字型態

typeof(NaN) // number

然後要再提一次,要判定 NaN 的方法:isNaN()

要轉換文字成為數字時,會用 parseInt()、parseFloat(),而這方法會由左到右依序辨認,若是無法辨認,則以已辨認的部分為準:

parseInt('100,000') //100, 因為無法辨認逗點
parseInt('$100,000') //NaN, 因為第一個值就無法辨認

快速轉換法:前面放個「+」號

+'9527' //9527

 

˙Date

若是用以下的方式產生新日期:new Date(year, month[, day[, hour[, minutes[, seconds[, milliseconds]]]]]),則會在 month 的地方出錯:

new Date(2016, 10) //Tue Nov 01 2016 00:00:00 GMT+0800 (台北標準時間)
new Date(2016, 10, 30) //Wed Nov 30 2016 00:00:00 GMT+0800 (台北標準時間)
new Date(2016, 10, 31) //Thu Dec 01 2016 00:00:00 GMT+0800 (台北標準時間)

這是因為 month 的表示方法,相較於 1 ~ 12 月,對應的值是 0 ~ 11 ..... 

日期轉換問題

new Date('2016-11-30') //Wed Nov 30 2016 08:00:00 GMT+0800 (台北標準時間)
new Date('2016/11/30') //Wed Nov 30 2016 00:00:00 GMT+0800 (台北標準時間)

若用 yyyy-MM-dd 格式,由於台灣是 GMT+8 時區,因此會自動加 8 小時,可想見,若是有些地方是 GMT-8,則顯示日期可以能會是前一天的日期

ECMA 規格中定義的最正式寫法

YYYY-MM-DDTHH:mm:ss.sssZ
2016-09-25T02:24:39.385Z

參考:保哥、前端工程研究:關於 JavaScript 中 Date 型別的常見地雷與建議作法

 

˙Hoisting 

中文翻譯叫做提升,大部分直接用英文,是一種「把宣告提升到其所在區域內頂端的行為」,而這邊要提到,js 的變數,是 function scope:

if(true) {
  var a = 1;
}

console.log(a) //1

在其他語言中,a 值得顯示一定會出現 error 的,但在 js 中,卻可以成功執行;如此可知,限制變數的最大阻力來自於你的心 function,而非大括號。

回到 hoisting,js 會將所有的 var 變數宣告以及 function 宣告提到最前面,為了避免 var 宣告變數會在程式運作中間產生差異,應該一開始就在函式的最前方把所有的變數宣告出來。

var i = 5;

function(){
  if(i < 10) {console.log('BOOOM!');} //這邊 i 是全域
}

可見到在上面的 function 中,一開始預期要取得全域 i 的值,然後做出判斷;但若是別人在不知道的情況,改寫了 code:

function(){
  if(i < 10) {console.log('BOOOM!');}
  // .... 
  
  var i = 5 //相當於在 function 最前方宣告 var i
  // ..... 
}

此時 i 的宣告會被拉升到該 function 的最上方,導致前面的判斷式失敗。

而雖然 function 的宣告也會被提到最前方,但由於 function 的敘述都很長,因此為了開發、閱讀維護之便利,一個js 檔的寫法可能會如下:

// 變數宣告
var a = 1, b = 2, c = setting || 3, d = function(){....};

// 主程式邏輯數十行
a++
b--
something(c);

// function 宣告
function something() {...}
function another() {...}

接著由於要防止自己寫的 js 汙染到全域的變數或是其他人的 js,我們會在最外層會用 IIFE 的方法包起來

(function(){....})([window])

到這邊為止,可以再重看一次一道常被人轻视的前端JS面试题

 

˙Closure(閉包)

當我們要做一個可以記錄目前狀態的函數的時候,常常會使用全域變數來做這件事情。

Ex.紀錄點擊數,並呈現出來
直覺解法:寫一個 function,外頭放一個全域變數來記錄

var cnt;
function clickButton() { 
  //... 
  cnt++; 
}

但在同時,我們也不想要汙染到全域變數,因此,運用 closure 的概念來寫:

function clickButton() {
  var cnt = 0;
  return function(){
    return cnt++;
  }
}

var a = clickButton();
a() //0
a() //1
a() //2
...

 

˙Javascript 物件導向

來個口令:建構式就是函式,函式就是建構式,但到底是函式還是建構式,就要看用的人是怎麼用了。

這個意思是說,你為了要寫相當於 java class 格式的「建構式」,或是只是普通使用的「函式」,別人用的時候有可能把你的建構式當成函式使用,或是把你的函式當作建構式使用,你,沒有選擇的權利!對於使用的人來說,當他使用了 new,那麼該 function 對他來說就是「建構式」,而若是他直接加上 (),那麼那個 function 就是「函式」。

結論就是:別用溝通!事先寫的跟使用的人要配合好,不然會造成災難

// 有個不知道是建構式還是函式,拿來用用看
function Car(name) {
this.name = name;
}

//建構式用法
var myCar = new Car('BMW'); //此時Car()為建構式
// 此時 myCar 的上層物件是 Car.prototype 
myCar.name //BMW

//函式用法
var urCar = Car('FIAT'); //此時Car()函式
urCar.name //undefined
window.name //FIAT

若一個 function 成為了構造式,則裡面的回傳值視情況有不同的回傳情況
1.無回傳 --> 回傳實例物件
2.回傳基本類型 --> 回傳實例物件
3.回傳引用類型 --> 回傳引用類型的實例物件

function foo() {}
new foo(); //foo{}

function goo() {return 1;}
new goo(); //goo{}

function hoo() {return {name: 'cooool'}; }
new hoo(); //{name: 'cooool'}

要注意的是,若是回傳值是 this,那又有另一番解釋。

建構式的情況下,相當於物件實體,也濟世會回傳實例物件(foo{}),

若被當成涵式使用,則 this 就會試根物件(瀏覽器中的 window),所以在 Car('FIAT') 的案例中,window 會被汙染。

 

˙實際運用

課堂所學,要如何運用於工作上?
依照課程所學,有很多之前寫過的 code 可以做重構,或至少重新 code review:
IIFE 的寫法可以將之前所寫的 js,全部打包過一遍。
closure 可以將一些部份特別打包起來。
Hoisting 的概念則將 js 的寫作順序作變更:變數宣告、主要邏輯、函數式宣告。

 

arrow
arrow

    angelyeah 發表在 痞客邦 留言(3) 人氣()