2016年3月24日

[筆記] 談談 JavaScript 中的 "this" 和它的問題

img
在 JavaScript 中有一個很特別、很常用又常常讓初學者很困擾的東西 ─ this,在這堂課中會來談談這個讓人又愛又恨的 this
this 通常會指稱到一個物件,同時 this 會在不同的情境下指稱到不同的物件。 讓我們來看幾個不同的情境,幫助我們更了解 this

Global Object (Window 物件)

這裡我們在三種不同情境去呼叫 this,分別是在程式的最外層(outer environment)直接去執行;使用function statement 去執行;使用 function expression 去執行(如果還不清楚 function statement 和 function expression 的差別,可以參考註1)。
// Outer Environment
console.log(this);

// function statement
function thisInFunctionStatement() {
  console.log(this);
}
thisInFunctionStatement();

// function expression
const thisInFunctionExpression = function() {
  console.log(this);
}
thisInFunctionExpression();
結果會發現,這三個 this 都會指稱到同樣的記憶體位置,也就是全域環境(global environment)中的 global object(在瀏覽器的環境下,global object 只的就是 Window 物件):
Imgur
這也就是說,我們可以直接利用這個 function 和 this 在 Window 物件建立新的屬性:
function addNewPropertyInWindowObject() {
  console.log(this);
  this.newPropertyInWindow = 'Create a new property.'
}

addNewPropertyInWindowObject();
console.log(newPropertyInWindow);   // Create a new property.
在這裡我們利用 this.newPropertyInWindow = "..." 來在 Window 物件中添加新的屬性,程式的最後,則可以直接 console.log(newPropertyInWindow),這裡之所以可以不用打 this.newPropertyInWindowwindow.newPropertyInWindow 是因為任何在全域環境(global environment)下 Window 物件的屬性,都可以直接去指稱它,而不用使用 dot-notation( .)去指稱。
跑出來的結果會像這樣子:
Imgur
透過 console.log(newPropertyInWindow) 會呼叫出該屬性的值- "Create a new property",同時,在Window 這個落落長的物件中,我們也會找到 newPropertyInWindow 這個屬性:
Imgur
❗️在瀏覽器的執行環境下,global object 指的就是 Window 物件;在 NodeJS 的執行環境下 global object 則是指稱 Global 物件。

Method in object

我們知道,在物件裡的值如果是原生值(primitive type;例如,字串、數值、邏輯值),我們會把這個新建立的東西稱為「屬性(property)」;如果物件裡面的值是函式(function)的話,我們則會把這個新建立的東西稱為「方法(method)」。
在這裡,我們就要來建立物件中的方法:
const objectWithThis = {
  name: 'I am the object',
  log: function () {
    console.log(this);
  }
}

methodInObject.log();
首先,我們利用物件實體語法(object literal)的方式建立一個名為 objectWithThis 的物件(如果不清楚用「物件實體語法」來建立物件的方式,可以參考註 2),裡面包含了名為 name 的屬性和名為 log 的方法。其中 log 是一個 匿名函式(anonymous function),程式內容很簡單,就是呼叫出 this 而已(關於匿名函式可參考註 1)。最後則是使用 objectWithThis.log() 的方式來執行該方法。
猜猜看,這時候的 this 會是什麼呢? 答案是物件 objectWithThis當這個函式是物件裡面的 method 時,這時候的 this 就會指稱到包含這個 method 的物件
Imgur
當某個函式是放在某一個物件裡面時,那麼該函式裡面的 this 指稱的就是該物件本身。

JavaScript 中關於 this 的一個問題

讓我們更進一步延伸來看這個範例:
const objectWithThis = {
  name: 'I am the object',
  log: function () {
    this.name = "Update current object name"    // 修改 this.name 的名稱
    console.log(this);
  }
}

objectWithThis.log();
假設我們在objectWithThis 物件內的 log 方法中多了一行 this.name = "Updated this Object name",因為我們知道 this 現在指的是物件 objectWithThis,所以可以想像的,當我執行這個 log 這個方法的時候,它就會去變更 objectWithThis.name 的值,所以:
Imgur
這個部分是沒有什麼問題的。
但假設我在 log 方法裡面,另外建立一個函式叫做 setName,一樣是用 this.name = 'newName' 的方式來修改這個 objectWithThis 物件中 name 的屬性值時。最後再去呼叫 this 來看一下,也就是程式碼改為:
const objectWithThis = {
  name: 'I am the object',
  log: function () {
    this.name = 'Update current object name'
    console.log(this);

    const setNameWithFunction = function(newName) {
      this.name = newName;
    }
    setNameWithFunction('Update object name to "NEW NAME"');
    console.log(this);
  }
}

objectWithThis.log();
你會發現結果 objectWithThisname 屬性的值並沒有被再次修改,沒有被改成預期的 "Update object name to 'NEW NAME'":
Imgur
怎麼一回事呢?
透過 console.log(window) 回頭仔細看一下 Window 物件,會發現在 Window 物件中發現了一個新的屬性 name,而且值是 "Update object name to 'NEW NAME'":
Imgur
也就是說,原來我們剛剛在函式 setNameWithFunction 裡面的 this,指稱到的是 Window 物件(global object),而不再是剛剛名為 objectWithThis 的物件 !
Imgur
我們可以在 setNameWithFunction 這個方法中,用 console.log(this) 來確認一下:
const objectWithThis = {
  name: 'I am the object',
  log: function () {
    this.name = 'Update current object name'
  console.log(this);

    const setNameWithFunction = function(newName) {
      this.name = newName;
      console.log(this);   // 確認 this 指稱的對象
    }
    setNameWithFunction('Update object name to "NEW NAME"');
  console.log(this);
  }
}

objectWithThis.log();
結果如下,在 log 這個方法中,我們一共執行了三次的 console.log(this),第一個和第三個 this 指稱到的是物件 objectWithThis 沒錯,但第二個在 setNameWithFunction 中的 this,指稱到的卻是 Window 物件(global object),而這也就是為什麼 setNameWithFunction 這個方法沒辦法幫我們修改物件 objectWithThisname 屬性的值的關係,因為 this 根本沒指稱到物件 objectWithThis
Imgur
而許多人認為,這是 JavaScript 長久以來存在的一個問題。

怎麼解決這個問題

那麼碰到上述這個例子時,我們可以怎麼做來避免指稱到不同的物件呢?
許多人的解法是這樣的,因為我們知道物件在指稱的時候是 by reference 的方式(不同的變數實際上是指稱到同一記憶體位置,可參考註 3),所以我們可以這樣做:
  • STEP 1:在整個 function 的最上面加上一行 var self = this(有些人會用 var that = this)。由於 by reference 的特性,selfthis 會指稱到同一個記憶體位置,而 this 指稱到的是原本預期該指稱到的物件 objectWithThis,所以 self 一樣會指稱到物件 objectWithThis 的記憶體位置。
  • STEP 2: 接著,把方法 log 內原本使用的 this 都改成 self,這樣做可以確保 self 指稱到的是物件 objectWithThis 而不用擔心會像上面的例子一樣指稱到非預期的物件。
Imgur
結果也如同我們預期的,在第二次 console.log(self) 的時候,就再次替換了物件 objectWithThisname 屬性的值。
Imgur

總結

  1. 如果是在 global environment 建立函式並呼叫 this,這時候 this 會指稱到 global object(在瀏覽器的環境下就是 Window 物件)。
  2. 如果是在物件裡面建立函式,也就是方法(method)的情況時,這時候的 this 一般就會指稱到包含這個方法的物件(之所以說「一般」是因為除了上述「問題」的情況之外)。
  3. 碰到物件方法中可能會有不知道 this 指稱到什麼的情況時,為了避免不必要的錯誤,我們可以在該方法中的最上面建立一個變數,先去把原本的 this 保留下來(var self = this;)。
  4. 如果真的還是不知道那個情況下的 this 會指稱到什麼,就 console.log 出來看看會是最好的辦法!
如果真的還是不知道那個情況下的 this 會指稱到什麼,console.log 出來看會是最好的辦法!

程式範例

const objectWithThis = {
  name: 'I am the object',
  log: function () {
    var self = this;    // 先透過 self 把原本的 this 存起來
    self.name = 'Update current object name'

    const setNameWithFunction = function(newName) {
      self.name = newName;
      console.log(self);
    }

    setNameWithFunction('Update object name to "NEW NAME"');
    console.log(self);
  }
}

objectWithThis.log();

延伸閱讀

備註

資料來源

2016年3月13日

[筆記] 談談 JavaScript 中 by reference 和 by value 的重要觀念

img
圖片來源:Udemy
在這堂課中,我們會說明在 JavaScript 中很重要的一個觀念,也就是說明什麼是 by value 和 by reference,正確了解這個觀念,能夠在寫程式的時候避免不必要的 bug 發生。
那我們先來看一段簡單的程式,接著再繼續說明 by value 和 by reference 的觀念。
我們先建立一個變數 a,並且值為 3 (var a = 3);接著,我們在建立變數 b,把 b 的值等於 a (b = a);最後,我們再把 a 的值改為 2 (a = 2)。
img
然後,我們把 a 和 b 呼叫出來,你猜猜看會得到什麼值:
img
這時候,我們會發現 a 的值變成了 2,而 b 的值仍然是 3,感覺好像蠻直觀的對吧。
可是,奇妙的地方讓我們繼續看下去:
我們一樣先建立一個變數 c,它是一個物件;接著建立變數 d,讓 d = c;最後我把物件 c 裡面的值做一下修改,從 Hello 改成 Hola;最後再把結果顯示出來:
img
你認為結果會是什麼呢?
img
看出有什麼特別之處了嗎?
在第一個例子中,我們將 a 變數的值做了修改之後,並不會影響到 b 的值;可是在第二個例子中,我們將 c 物件的值做了修改之後,連帶的影響到了 d 物件的結果!這就是我們在這篇文章中要說明的重要觀念!

By Value vs By Reference

By Value

讓我們先來說明一下什麼是 by value 。
當我們在建立 primitive type 的變數時(數字、字串、布林),假設我把a 指定成一個 primitive type 的時候,a 會在記憶體中存在一個自己的位置(假設叫做0x001)。這時候,當我指定另一個變數 b,它的值等同於 a 的時候,b 實際上會建立另一個獨立的記憶體位置(假設叫做0x002),接著再把 a 的值存在這個獨立的記憶體位置。也就是說, a 和 b 其實是存在於兩個不同的記憶體位置,因此彼此並不會乎相干擾影響,這種情況,我們就稱為 ++By Value++ ,而這種情形會發生在 primitive type 的變數。
img
在 JavaScript 中 primitive type(Boolean, String, Number, null, undefined)都屬於 By Value。

By Reference

在來看一下 By Reference。
當我將變數 a 設立成一個Object(或function)時,這時候,一樣會在記憶體中給它一個位置(假設叫做0x001);但是當我建立一個變數 b,並且把變數 b 的值等同於 a 時,這時候並不會再給它一個新的位置,而是一樣指定到物件 a 的位置(即0x001),實際上是不會有新的東西被建立,變數 a 和 b 都會被指稱到相同的位置(即0x001),因此,當 a 的值改變的時候 b 的值也會改變,因為它們實際上是指稱到相同的位置,這種情形我們就稱為 ++By Reference++,這樣情況會發生在 Object 和 Function 這種變數
img
經過這樣的說明,我想你就知道在上面的例子中,為什麼第一個例子改變(mutate)a 的值的時候,b 不會跟著改變;而改變 c 的值的時候,d 卻會跟著改變了吧。因為 a 和 b 是屬於 primitive type 的變數,而 c 和 d 則是 object。
一般來說,Primitive type 是 by value,而 Object 和 Function 則是 by reference,但有例外的情況,我們等等會看到。
這樣的規則即使是在 function 裡面的參數(parameter)也是一樣的,
讓我們再來看個例子,上面的部分是剛剛的例子二,我們在例子二的下面,建立一個 function,這個函式中名稱屬性的值為 changeGreeting ,裡面可以放參數 obj,程式內容屬性的值則是用來修改物件當中 greeting 屬性的值:
img
在這個例子中,我們可以看到,原本 c 和 d 物件中屬性 greeting 的值都是Hello,後來因為我修改了 c.greeting 的值為 Hola,所以 c 和 d 物件屬性 greeting 的值都變成了 Hola,最後我利用 function 的方式,改變 d 修改了 d.greeting 的值為 Hi,所以到了最後 c 和 d 物件屬性 greeting 的值都變成了 Hi。這就是因為 By reference 的緣
故!
img
在 JavaScript 中 Objects(Object, Array, Function)都屬於 By Reference。

例外情況

再來,有一個很重要的例外,如果我是用 object literal 的方式指定物件的值,那麼就會是 by value,那我們來看這個例外的情況:
img
在這裡,如果我們是用 object literal 的方式去定義 c 這個變數(如果不清楚什麼是 object literal,可參考:[筆記] JavaScript中的物件建立(Object) - Part 2),在這種情況底下,因為它並不清楚 c 的內容是已經存在的,所以它會建立一個新的記憶體位置來存放 c 物件裡面的內容。
img
若是使用 object literal 的方式來建立物件,則會變成 by Value,新增了一個記憶體的位置。

程式範例

// by value (primitives)
var a = 3;
var b;

b = a;
a = 2;
console.log("a is " + a);
console.log("b is " + b);


// by reference (all objects (including functions))
var c = {greeting : 'Hello'};
var d;
d = c;

console.log(c);
console.log(d);


c.greeting = "Hola";

console.log(c);
console.log(d);

// by reference (even as parameters)
function changeGreeting(obj){
  obj.greeting = 'Hi'; //mutate
}

changeGreeting(d);
console.log(c);
console.log(d);


var c = {greeting : 'Hello'};
var d;
d = c;
console.log(c);
console.log(d);

// equal operator sets up new memory space (new address)
c = {greeting: "Hola"};
console.log(c);
console.log(d);

JavaScript 不是 by value 也不是 by reference!?

這是最近在 fb 有很多人在討論的,最後大家的結論認為 JavaScript 實際上是另一種稱做 "by sharing" 的模式,只是對於原生值來說,看起來會很像 by value,對於物件來說則會很像 by reference。因此,如果對於這一部分有興趣的朋友們,或者在看完這篇文章後還是不很清楚 by value 和 by reference 的話,建議也可以閱讀下方的延伸閱讀。

延伸閱讀

資料來源

2016年3月10日

[筆記] 進一步談JavaScript中函式的建立─function statements and function expressions

img
圖片來源:Udemy
在這堂課中,我們會說明 function statements 和 function expressions 這兩種不同建立函式的方式,這是兩個許多新手在學 JavaScript 會有些搞不懂的地方。
在進入到 function 的部分前,先來看看表達式(expressions)和陳述句(statements)有什麼不同吧。

表達式(Expressions)和陳述句(Statements)的差異

表達式(Expressions)

Expressions 指的是輸入後能夠直接回傳值的一串程式(a unit of code that results in a value),我們一般可能會把它存成一個變數,但是它不一定要被存成一個變數
簡單來說,只要你輸入的那串程式執行後能直接回傳一個值,那麼它就是個 expression。
舉例來說,在瀏覽器的 console 中輸入 a = 3 時,它會直接回傳 3 這個值;輸入 2 + 3 的時候,它會直接回傳 5;輸入 a = { } 的時候,它會回傳一個為物件的值。這種輸入一段程式後,會直接取得回傳一個值的程式內容,我們就稱為 Expressions。
Imgur

陳述句(Statements)

那麼陳述句(Statements)是什麼呢?
我們來看看這段程式:
if (a === 3) {
  console.log('Hello');
}
在這段程式中 a === 3 是一個表達式(expression),因為它可以直接回傳值(即,truefalse);而 if 這個指令,則是一個 statement,因為它不會直接回傳一個值,我們也不能將它指定為一個變數:
// ❌ 錯誤寫法
const b = if (a === 3) {
  console.log('Hello');
}

Function Expressions 和 Function Statements

在 JavaScript 中,Function 就是物件的一種,它可以透過 Expression 或 Statements 的方式加以操作。

Function Statements

首先,我們來看一下Function Statements:
// Function Statements
function greet() {
  console.log('Hi');
}
這是一串 Function Statements,它不會直接回傳任何的值。Function Statements 的特色在於,它在程式執行的最開始,該函式就會透過 hoisting 先被儲存在記憶體中(如果不清楚 Hoisting 的概念,可以參考:筆記 - 談談Javascript 中的 Hoisting),也就是說我們可以在執行這段程式前,就去呼叫這個函式來使用而不會出現任何的錯誤,像是這樣:
/* Function Statements will be hoisted */
greet();   // 'Hi'

function greet() {
  console.log('Hi');
}
由於在 JavaScript 中 function 就像物件一樣,用物件的概念來理解函式的話,這個函式屬性 name 的值是 greeting,這個函式 code 屬性的值為 console.log('hi')
imgur
當我們要執行這個函式的時候,只要輸入 greet() 就可以了。
如果我們用 console.log(greet),會得到這個函式的程式內容(code),所以如果我們想要執行這個函式,就在最後面再加上 ( ) 就可以了:
Imgur

Function Expressions

再來看一下 Function Expressions,我們先前有提過在 JavaScript 中 Function 就是物件的一種,所以我們可以把它存在一個變數中。以下面的程式碼為例:
// Function Statements
const sayHello = function() {
  console.log('Hello');
};
這裡的 function(){ ... } 這段就是 Function Expression,現在我們則把這個函式表達式的值存在 sayHello 這個變數內。
從物件的角度來看函式的話,在這個例子中,就是先建立了一個函式,但在這個函式的 name 屬性並沒有給它值(因為我們在括號前面並沒有給任何名稱),之所以可以這麼做是因為,我們在 function expression 前面已經把它指定到一個變數(sayHello)了,所以可以直接用這個變數名稱來指稱這個函式。對於這種 name 屬性沒有值的函式,我們可以稱作匿名函式(anonymous function 或 function literal)
imgur
圖片來源:Udemy - JavaScript: Understanding the Weird Parts,此圖的函式名稱為anonymousGreet
同樣的,如果我們想要執行這個函式,一樣輸入 sayHello() 就可以了。
當我們在瀏覽器的開發者工具中輸入 console.log(sayHello) 時,我們一樣會得到這個函式的程式內容,因此,若我們想要執行這段程式內容,同樣只要在最後面加上( ) 就可以了:
Imgur
然而,和 Function Statements 不同的地方是,因為在一開始執行程式初期,只會先建立並儲存變數名稱到記憶體中,也就是只會儲存 sayHello 到記憶體中,但程式內容不會一併儲存進去(這時候 sayHello 的值會是 undefined),所以如果我在 function 定義前面就想要執行它的話,即會出現錯誤訊息:
// ❌ 錯誤寫法
// ReferenceError: Cannot access 'sayHello' before initialization

sayHello();   // 使用 Function Statements 的話不能在定義前呼叫它

// Function Statements
const sayHello = function() {
  console.log('Hello');
};
imgur

再來看一點特別的吧 - 函式中的函式

看一下這段程式。先建立一個函式,name 屬性的值是 logcode 的內容是 console.log(a),其中 a 是這個函式的參數。
接著,我分別去執行 log(...),會分別得到如下註解的結果:
function log(a) {
  console.log(a);
}

log(3); // 3
log('Hello'); // 'Hello'
log({ Country: 'Taiwan' });   // { Country: 'Taiwan' }
同樣地,我們也可以先把這些值指定成一個變數,然後再丟到函式當中,會得到一樣的結果:
function log(a) {
  console.log(a);
}

const number = 3;
log(number); // 3

const hello = 'Hello';
log(hello); // 'Hello'

const country = { Country: 'Taiwan' };
log(country); // { Country: 'Taiwan' }

如果這時候我的值是函式的話呢

假設我們在 log() 裡面,放入一個 function expressions,而且是一個匿名函式(anonymous function),這時候我們等於是直接創造了一個函式來使用(create the function on the fly):
log(function() {
  console.log('Hello');
});
如果看不太懂的話,可以想成這樣會比較容易理解:
const anonymousFunction = function() {
  console.log('Hello');
};

log(anonymousFunction);
這時候會回傳這樣的結果:
Imgur
那麼,如果我們希望能夠直接執行該函式,我們可以將 log( ) 做一下簡單的修改就可以了:
function log(a) {
  a();
}

const anonymousFunction = function() {
  console.log('Hello');
};

log(anonymousFunction);   // Hello
這樣,就可以在不用建立函式的情況下,直接去執行一個匿名函式。
由於 JavaScript 非同步的特性,若想要確保程式執行的順序,常常會使用到回呼函式(callback function)這種方式,它內部的做法其實也就是把函式傳入另一個函式中去呼叫。
這種傳入一個函式到另一個函式中去呼叫的方式在 JavaScript 中經常會使用到,特別是回呼函式(callback function)。

程式範例

// Function Statements
function greet() {
  console.log('Hi');
}

// Function Expressions
var sayHello = function() {
  console.log('Hello');
};

function log(a) {
  a();
}

log(function() {
  console.log('Hello');
});

資料來源

Udemy - JavaScript: Understanding the Weird Parts- Function Statements and Function Expressions

[筆記] JavaScript 中函式就是一種物件 ─ 談談 first class function(一等公民函式)

在這堂課中,作者說明了一個很重要的觀念,也就是在 JavaScript 中,函式也是物件的一種(functions are object)

First class functions

First Class Functions(一級函式),指的是任何你可以對函式做出任何對其他型別(Objects, String, Boolean, Numbers)也做得到事,包括將 Function 指定成一個變數,帶入另一個函式中等等 。
JavaScript 中的 function 就符合 First Class Functions 這樣的特性:
  • 函式只是物件的一種
  • 可以將 function 儲存成變數
  • 可以將 function 當成參數代入另一個 function 中
  • 可以在一個 function 中回傳另一個 function
  • function 跟物件一樣有屬性(property)

函式也是物件的一種

這句話到底是什麼意思呢?
它指的是在 JavaScript 中,我們把 function 想成就是一個物件,其中這個物件包含了兩個比較特別的部分,一個是名稱(name),一個是執行的程式內容(code)
其中,function 的名稱是可有可無的,它可以是一個匿名函式(anonymous function);而程式內容的部分,我們則是可以透過 () 來加以執行(invoke)
讓我們來看一下這段程式:
function greet() {
  console.log('Hello');
}

greet.language = 'english';
首先,我們建立一個函式,它的名稱叫做 greet,而它的程式內容是 console.log('Hello')
另外,因為 function 可以當作物件來使用,所以可以直接用「.」來建立該物件的屬性和值(若對物件的概念還不清楚,建議可參考:[筆記] JavaScript中的物件建立(Object) - Part 1),屬性的名稱為 language,值為 englsih。
如果我們想要呼叫這個函式,我們只要打該函式的名稱,後面接上括號 () 去執行該函式:
greet();
如果我是輸入 console.log(greet),則是會顯示該函式的程式內容:
如果我是輸入 console.log(greet.language),則是會顯示 greet.language 的值:
console.log(greet.language);   // english
這樣的例子說明了,function只是一種特殊的物件,它可以被當作物件來使用。

資料來源