The Weird Part Of Javascript - part 2

tags: Javascript

物件與函式 (Objects And Functions)

在其他程式語言中物件、函式是兩種完全不同的存在,但是在JS裡面它們非常相似

Objects And The Dot

  • 物件(objects)是鍵值配對 (name:value pair) 的集合(不一定只有一對)
  • 物件內可以包含有純值(Primitives)、另一個物件(Objects)、函式Function(或稱方法method)
  • 上面標示的0x001是記憶體內部位置的範例,就像是它們的地址

範例

這邊是為了作範例而這樣製作物件,不過會有更好的方式會在後面的章節介紹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var person = new Object();

person["firstname"] = "Tony";
person["lastname"] = "Alicea";

var firstNameProperty = "firstname";

person.address = new Object();
person.address.street = "111 Main St.";
person.address.city = "London";


console.log(person); //得到object
console.log(person["firstname"]); //得到Tony
console.log(person[firstNameProperty])//得到Tony
console.log(person.firstname);//得到Tony
console.log(person.address.street); //得到111 Main St.
console.log(person.address.city);//得到London
  1. 我們設置一個物件person,並且新增兩個屬性firstname,lastname
  2. 把firstname指派給變數,一樣可以使用變數取得新增的內容
  3. 使用(.)可以更方便的新增屬性以及取得物件內容
  4. 使用 [ ] , (.) 這兩個運算子都能取得物件內容以及新增屬性

[ ] 運算子

  • 需要使用字串型態
  • 可使用變數指派的方式使用

(.) 運算子

  • 編譯器可以直接閱讀不需要使用字串型態
  • 不可使用變數指派方式使用
  • 更簡潔易讀

物件、物件實字(Objects And Object Literals)

  • 為了創造物件我們可以使用{ } 是 new Object();的縮寫
  • {} 不是運算子
  • JS引擎會判斷使用{ } 就是正在創造物件
  • 只要用{ } 來建立物件的語法,就稱為物件實字 (Object Literals)
1
2
3
4
5
6
7
8
var Tony = {firstname:'Tony',lastname:'Alicea'};
console.log(Tony);

var Tony = new Object();
person.firstname = "Tony";
person.lastname = "Alicea"

console.log(Tony);

會印出一樣的結果

物件創建在物件之內

1
2
3
4
5
6
7
var Tony = {
firstname:"Tony"
address:{
street:'111 Main St.',
city:'New York'
}
}

創建在函式內部

1
2
3
4
5
6
7
8
function greet(person){
console.log('HI' + person)
};

greet({
firstname:'Mary',
lastname:'Doe'
});

使用在增添屬性上面

1
2
3
4
Tony.company = {
street: '333 Second St.',
companyName: 'Sucess'
}

框架小叮嚀(Framework Aside)

  • JS是沒有namespace,因為{ }的關係不需要
  • 可以使用{ } 創造出物件來假扮命名空間
  • 在框架或是函式庫的原始碼中很常看到這樣的使用方式

Faking Namespaces

命名空間(Namespace)

專門給變數以及函式使用的空間,讓同樣名字的變數或是函式可以做區隔

範例

兩個變數名稱一樣時上方的變數會被複寫,因此印出Hola!

1
2
3
4
var greet = 'Hello!';
var greet = 'Hola!';

console.log(greet); // Renders Hola!

為了避免上面的複寫情況發生,我們可以使用Faking Namespaces,創造一個物件來包裹住這些變數,這樣就能避免變數或是函式之間名字相同的衝突或是複寫的狀況發生摟!

1
2
3
4
5
6
7
8
var greet = 'Hello!';
var greet = 'Hola!';

var english = {};
var spanish = {};

english.greet = 'Hello!';
spanish.greet = 'Hola!';

範例二

命名空稱使用的{ }物件可以做很多層:

把greet包裹在greeting裡面

1
2
3
4
5
6
7
var greet = 'Hello!';
var english = {};

english.greeting = {};
english.greeting.greet = 'Hello!';

console.log(english);

也可以使用物件實字:

1
2
3
4
5
6
7
8
var greet = 'Hello!';
var english = {
greeting:{
greet:"Hello!"
}
};

console.log(english);

JSON以及物件實字(JSON And Object Literals)

  • JSON (JavaScript Object Notation) / JS 物件符號
  • 跟物件的型態非常相似
  • 有數個方法可以使用來轉換JSON

一般物件型態:

1
2
3
4
var objectLiteral = {
firstname: 'Mary',
isAProgrammer: true
};

JSON型態:

1
2
3
4
{
"firstname": "Mary",
"isAProgrammer": true
}

轉換物件成JSON格式可以使用JSON.stringify()

1
JSON.stringify(objectLiteral);

轉換JSON為物件給JS使用JSON.parse()

1
var jsonValue = JSON.parse('{ "firstname": "Mary", "isAProgrammer": true }');

函式就是物件(Function Are Objects)

First Class Functions

  • 你可以對函式做對於其他類型(字串、數字、物件、布林值等)都可以做的事情
  • 可以指派函式為變數
  • 可以把函式當成參數給其他函式使用
  • 可以在literal syntax中使用函式

函式是一種特殊的物件,但正因為它是物件所以他可以使用純值、物件、函式以及附加的兩種特殊的屬性:

  1. name (非必須,有匿名函式)
  2. code 也就是使用者撰寫的程式碼,並且它是可以被呼叫的 “Invocable”()

MDN 一級函式

範例

能成功地給函式加上屬性代表函式真的是一種物件

1
2
3
4
5
6
7
function greet() {
console.log("h1");
}

greet.language = 'English';

console.log(greet.language);
  1. 設置一個函式greet,內容為印出hi
  2. 給函式加上屬性
  3. 印出greet.language

得出結果 正是加上去的屬性

“Invocable”()

  • 當創造這個greet 函式時,它會被放到記憶體裡(以目前的例子會放到全域物件裡)
  • 函式會有個名字屬性 greet
  • 函式會有code屬性也就是 console.log("h1");
  • 然而當呼叫greet()這邊使用括弧來呼叫函式

作者非常強調

JS的函式就是物件

函式陳述式、函式表達式(Function Statements And Function Expressions)

表達式(Expression)

  • 它不必須存在變數之中
  • 一段會創造值(value)的程式碼

表達式範例

  • (=),(+)運算子都會回傳結果,因此他們兩個都是表達式

var a;

  • 只要有回傳值就是表達式(下方回傳物件)

陳述式(statement)

判斷式if 就是個很好的例子

  • 不會返回值
  • 無法把if判斷式指派給變數
1
2
3
if(a ===3){

}

函式陳述式範例

  • 一開始就會被寫進記憶體中
  • 具有Hoisting特性
1
2
3
function greet() {
console.log('h1');
}

這段函式它不會回傳值因為它沒有被呼叫,所以它就是個函式陳述式,只代表它被放置於記憶體中,也就代表著Hositing

因此我們可以這樣使用:

  1. 先呼叫函式
  2. 撰寫函式本體
  3. 依舊可以印出結果
1
2
3
4
5
6

greet();

function greet() {
console.log('h1');
}

函式表達式範例

  • 一開始不會被寫進記憶體

  • 執行時建立這個函數物件使用指向該函數記憶體的變數進行呼叫(也就是指派給變數做呼叫)

  • 匿名函式的部分就是函式表達式

注意: 這邊可以發現匿名函式的部分就是函式表達式,因為它會產生值

  1. 創造匿名函式
  2. 把函式指派給變數 anonymousGreet
  3. 使用”()” anonymousGreet()
  4. 就可以呼叫此匿名函式瞜

#關於匿名函式的部分,其實可以命名,但是基於程式碼簡潔的關係以及其實函式位置已經綁訂於變數所以命名這部分是比較多餘的

函式表達式無法做Hoisting

  • 因為變數的預設值為undefined
  • 因為變數提升只會提升變數本身而不會提升值
  • 要到變數被執行了才會知道它的值,所以只會先顯示undefined那當然不是個函式
1
2
3
4
5
anonymousGreet();

var anonymousGreet = function () {
console.log('h1 ');
}

把函式作為參數丟進另一個函式

1
2
3
4
5
6
7
function log(a) {
a();
}

log(function () {
console.log('h1');
});
  • 把函式做為參數傳送
  • 這樣的寫法其實就是下方範例,也就是First Class Functions的概念
1
2
3
4
5
var a = function () {
console.log('h1');
}

a();

Conceptual Aside

By Value vs By reference

  • 這邊主要談論的都是指變數
  • reference 像是記憶體中的地址
  • value 代表變數的值

By Value

讓兩個變數有相同的value藉由複製value的方式但是有兩個不同的reference

  • 所有純值都是傳值(By value)
  1. 設置 a = 純值(數字、字串)
  2. 這時候純值會有個reference就像是它的地址讓變數a可以找到它
  3. 讓 新的變數b b = a
  4. 這時候b就會複製純值的value到不一樣的地址b

By Value 範例

1
2
3
4
5
6
7
8
9
// by value(primitives)
var a = 3;
var b;

b = a;

a = 2;
console.log(a);
console.log(b);

印出結果

因為by value只會複製值不會複製reference所以,b還是保持在新的地址,a的變化跟b無關

Mutate

改變某樣東西

  • Immutable 代表無法被改變

By reference

讓兩個物件有相同的物件藉由給予同樣的reference並不是複製同樣的內容

  • 所有的物件都是傳址(by reference)
  • 不管是處理把他們(物件)設置相等或是傳入函式
  1. 設置 a = 純值(數字、字串)
  2. 這時候純值會有個reference就像是它的地址讓變數a可以找到它
  3. 讓 新的變數b b = a
  4. 這時候b會藉由原本 a 的reference找到其value

By reference 範例

1
2
3
4
5
6
7
8
9
10
11
12
13

// by reference(all objects(including function))

var c = {
greeting: 'hi'
};
var d;

d = c;
c.greeting = 'hello';

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

印出結果:

因為By reference 傳遞的是地址,所以兩個物件c d 基本上是在一樣的地址一樣的內容,修改其一另一個一樣也被修改

By reference(even as parameters) 範例

1
2
3
4
5
6
7
8
function changeGreeting(obj) {
obj.greeting = 'Hola';
}

changeGreeting(d);

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

印出結果:

把物件使用參數做傳遞,一樣是傳址,因此兩個傳遞對象是一樣的地址,修改一個其他都會修改

By reference 使用(=)指派 範例(特例)

1
2
3
4
5
6
c = {
greeting: 'Howdy'
}

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

印出結果:

  • 這邊可以看到不是說reference是傳址,所以兩方物件應該會一樣?
  • 但是(=)運算子可以設定新的記憶體地址給c因此c,d印出來的結果不同了

物件、函式以及’this’(Objects, Functions, And ‘this’)

  • 函式就是物件: 其中有兩個特殊屬性 code, name
  • 當函式被呼叫時(也就是code的部分),會創造出執行背景(Execution Context),接著會被擺入執行堆(Execution stack),這會決定這個函式會如何被執行
  • 當執行背景被創造出來時,內部都會有variable Environment也就是變數被創造在函式內部
  • 也會有Outer Environment也就是當在函式內部找不到變數使用時,會往外部尋找參考一直找到全域變數為止(再來也沒了)
  • 但我們也知道每天JS引擎創造執行背景時都會創造’this’這個變數,甚至我們不需要輸入任何內容
  • 而這個this會指向(代表)不同的物件取決於這個函式是如何被呼叫

‘this’的指向

範例一

1
2
3
4
5
6
7
8
9
10
11
12
function a() {
console.log(this);
this.newvariable = 'hello';
}

var b = function () {
console.log(this);
}

a();
console.log(newvariable);
b();

這邊設置了三種情況

  1. 一定有的golbal object
  2. 函式陳述式
  3. 函式表達式

結果印出來:

全部都指向window這個global object
並且可以直接給global object加上屬性都沒問題

  • 當值是純值的時候被稱為property
  • 當值是函式的時候被稱為method

範例二(例外)

這邊把this使用在物件內部的函式也就是methods

1
2
3
4
5
6
7
8
var c = {
name: 'The c object',
log: function () {
console.log(this);
}
}

c.log();

印出結果:

竟然是指向了object

並且可以這樣使用

1
2
3
4
5
6
7
8
9
var c = {
name: 'The c object',
log: function () {
this.name = "I can change name"
console.log(this);
}
}

c.log();

印出結果:

竟然可以通過this的指向來操作物件的內容key的部分

範例三(類似bug)

於是我們找到一個類似於JS引擎比較類似缺點的地方:

透過函式表達式的方式使用變數傳遞函式在物件內部的methods內,並且使用this再次改寫一次name屬性,這邊理論上應該會使”I can change name”修改成’change name again’

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var c = {
name: 'The c object',
log: function () {
this.name = "I can change name"
console.log(this);

var setname = function (newname) {
this.name = newname;
}
setname('change name again');
console.log(this);
}
}

c.log();

印出結果:

沒有任何變化
剛剛以為透過物件內部的methods內部的this會指向物件本身,但是這邊的this卻指向別的地方

打開window全域物件查看發現,這邊的this竟然指向的位置是全域物件window

範例四(範例三的解答)

如何避免這樣的情況發生呢?

把this的位置好好綁訂好並且把每個地方的this都使用變數確認是使用同一個this指向同一個地方就可以解決這個問題摟!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var c = {
name: 'The c object',
log: function () {
var self = this;

self.name = "I can change name"
console.log(self);

var setname = function (newname) {
self.name = newname;
}
setname('change name again');
console.log(self);
}
}

c.log();

印出結果:

這次的this就正常的指向物件本身因此可以修改name屬性搂!

Conceptual Aside

Arrays Collection of Anything

Arrays是一個集合,聚集了很多東西在裡面

創造Array

  • 使用new Array();
    var arr = new Array();

  • 使用array literal syntax
    var arr = [];

Arrays 基礎

  • JS的array是以0為基底的:

console.log(arr[0])可以印出array第一個元素

  • 可以放入任何東西進去Arrays:

Number, String, Object, function(still Object), Boolean

1
2
3
4
5
6
7
8
9
10
11
12
13
var arr = [
1,
false,
{
name: 'Tony',
address: '111 Main St'
},
function (name) {
var greeting = 'Hello ';
console.log(greeting + name);
},
"hello"
];

印出結果:

正常印出不會報錯,JS中的Arrays可以放入任何東西並且不會報錯

  • 如果想要呼叫Arrays中的function並使用

arr[3](arr[2].name);

  1. 使用 arr[3]()呼叫函式
  2. 使用 arr[2]j.name使用物件內部的屬性

印出結果:

參數以及其餘參數(‘arguments’ And Spread)

Arguments

其實就是參數(parameters)的另一個說法,也就是傳入函式的資料

不輸入參數

如果我們呼叫這個函式但不輸入參數:

1
2
3
4
5
6
7
function greet(firstname, lastname, language) {
console.log(firstname);
console.log(lastname);
console.log(language);
}

greet();

會發生Hoisting讓這三個變數在記憶體佔據位置但因為還未賦值因此給予預設值undefined

印出結果

不完全輸入參數(只放一個或兩個)

在JS裡面函式可以不放入參數或者是只放入部分的參數

1
2
3
4
5
6
7
8
9
10
11
12
function greet(firstname, lastname, language) {
console.log(firstname);
console.log(lastname);
console.log(language);
console.log('-----------------');
}

greet();
greet('john')
greet('john', 'Doe')
greet('john', 'Doe','en')

印出結果:

會發現放入的參數會依序去對應設置好的變數

只放一個參數

放入兩個參數

放入全部參數

給參數放入預設值

在這邊給參數放入預設值,當呼叫函式並且沒有輸入參數內容則預設值就會啟動

  • ES6
1
2
3
4
5
6
7
8
9
10
11
function greet(firstname, lastname, language = "chinese") {
console.log(firstname);
console.log(lastname);
console.log(language);
console.log('-----------------');
}

greet();
greet('john')
greet('john', 'Doe')
greet('john', 'Doe', 'en')
  • ES5:

當language是 undefined就會輸入右邊的預設值,然而當左邊是字串時(也就是有填入正常的參數)則正常顯示其內容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function greet(firstname, lastname, language) {

language = language || "chinese"

console.log(firstname);
console.log(lastname);
console.log(language);
console.log('-----------------');
}

greet();
greet('john')
greet('john', 'Doe')
greet('john', 'Doe', 'en')

印出結果:

可以發現除了最後一個結果因為有輸入參數’en’,其它因為沒有輸入參數因此都填入預設值

放入特殊字元 arguments

現在已經不是最好的使用選擇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function greet(firstname, lastname, language) {

language = language || "chinese"

console.log(firstname);
console.log(lastname);
console.log(language);
console.log(arguments);
console.log('-----------------');
}

greet();
greet('john')
greet('john', 'Doe')
greet('john', 'Doe', 'en')

印出結果:

會得到一個 Array-like的東西但不是Array,但包含一些Array的特性

範例

如果想要讓沒有輸入參數的函式停止動作:

使用判斷式如果arguments的長度為0也就是沒有內容,則印出字串
‘Missing parameters!’以及分隔線並繼續下一個函式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function greet(firstname, lastname, language) {

language = language || "chinese"

if (arguments.length === 0) {
console.log('Missing parameters!');
console.log('-----------');
return;
}

console.log(firstname);
console.log(lastname);
console.log(language);
console.log(arguments);
console.log('-----------------');
}

greet();
greet('john')
greet('john', 'Doe')
greet('john', 'Doe', 'en')

印出結果:

成功讓沒有參數的函式印出字串

取得arguments的內容

arguments一樣是以0為基底

故使用 arguments[0] 即可以取得第一個內容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function greet(firstname, lastname, language) {

language = language || "chinese"

if (arguments.length === 0) {
console.log('Missing parameters!');
console.log('-----------');
return;
}

console.log(firstname);
console.log(lastname);
console.log(language);
console.log(arguments);
console.log('arg[0]: ' + arguments[0]);
console.log('-----------------');
}

greet();
greet('john')
greet('john', 'Doe')
greet('john', 'Doe', 'en')

印出結果:

將array -like物件轉換為array

將array -like物件轉換為array的方法:

1
2
Array.prototype.slice.call(arguments)
Array.from(arguments)

其餘運算子(rest operator)

其餘參數:將多個參數轉為陣列,用在不知道參數數量的情況

  1. 設置fun1(...args)代表不確定數量的參數
  2. 並使用fun1() - 不輸入參數呼叫
  3. fun1(1, 2, 3, 4, 5, 6, 7) - 輸入多個參數呼叫
1
2
3
4
5
6
7
8
function fun1(...args) {
console.log(args);
console.log(args.length);
console.log('----------');
}

fun1();
fun1(1, 2, 3, 4, 5, 6, 7)

印出結果:

會把不知道數量的參數轉為陣列

展開運算子(spread operator)

跟其餘參數的用法相反

展開運算子:展開運算子則是可以把陣列中的元素取出

Math.max()無法使用陣列操作,因此使用展開運算子取出裡面的數值做操作

1
2
3
4
let number = [1, 2, 3, 4, 5];

console.log(...number);
console.log(Math.max(...number));

印出結果:

會取得陣列中的內容

框架小叮嚀(Framework Aside)

函式多載(Function Overload)

是Ada、C++、C#、D和Java等程式語言中具有的一項特性,當多個同名函式的形式參數的資料類型不同或數量不同時,就構成了函式的多載。

簡單來說就是不一樣的函式以及參數(或是參數數量)卻有著一樣的名字

  • 在JavaScript中,同一個作用域,出現兩個名字一樣的函式,後面的會覆蓋前面的,所以 JavaScript 沒有真正意義的過載。
  • 但JS有First Class Functions可以把函式像純值或是物件一樣當作參數使用

範例說明

從範例一可以看出第二個greet函式結果取代了第一個,證明JS是沒有函式多載的

範例一 證明JS無函式多載

1
2
3
4
5
6
7
8
9
10
11
function greet(firstname, lastname, language) {
console.log('Hello ' + firstname + ' ' + lastname);
}

function greet(firstname, lastname, language) {
console.log('Hola ' + firstname + ' ' + lastname);
}


greet('john', 'Doe', 'en');
greet('john', 'Doe', 'es');

印出結果:

都是Hola john Doe,代表第二個函式覆蓋過第一個函式

範例二 使用JS仿造函式多載

使用if 條件式以及First class function 呼叫相同的函式來仿造函式多載

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function greet(firstname, lastname, language) {
language = language || 'en';

if (language === 'en') {
console.log('Hello ' + firstname + ' ' + lastname);
}

if (language === 'es') {
console.log('Hola ' + firstname + ' ' + lastname);
}
}

function greetEng(firstname, lastname) {
greet(firstname, lastname, 'en')
};

function greetEs(firstname, lastname) {
greet(firstname, lastname, 'es')
}


greetEng('john', 'Doe');
greetEs('john', 'Doe');

印出結果:

呈現出不同的內容

Conceptual Aside

語法解析器Syntax Parsers

我們寫的程式碼並不是直接被電腦讀取而是居中有一個”程式(program)” 轉譯我們的程式碼讓電腦可以理解,瀏覽器中的JS引擎就會做這些事

語法分析器便是JS引擎的一部分:

  • 它會解析使用者的程式碼一個字母一個字母的讀取
  • 如果結果不如他的預期會報錯
  • 如果符合預期則繼續
  • 它會使用者的程式碼做出預設立場並且制定規則甚至可以做出修改在其程式真正執行之前

Dangerous Aside

自動插入分號(Automatic Semicolon Insertion)

Always avoid it 作者說的

JS引擎中的syntax parsers做這件事情它認為是有幫助的

在JS中Semicolon;是非必要的,因為JS引擎會幫你做這件事

  1. 永遠記得自己使用;,因為妳不會想要JS引擎幫妳做這件事

範例

這邊return做了換行的動作會出現報錯

1
2
3
4
5
6
7
8
function getPerson() {
return
{
firstname: 'John',
};
}

console.log(getPerson());

因為自動插入分號的關係,在return的後方加入的分號導致出錯

印出結果:


因此確保大括號寫在return後方保障其不會幫它插入分號

1
2
3
4
5
6
7
function getPerson() {
return {
firstname: 'John',
};
}

console.log(getPerson());

印出結果:

框架小叮嚀(Framework Aside)

Whitespace

這篇主要是解釋 JS 引擎處理空白處是比較自由的,所以可以好好透過空白處來撰寫比方說:

  1. comment
  2. 筆記

從下方範例可以理解,空白處的用法,以及雙// 會被 JS 引擎忽略,依舊正常執行程式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

var
//firstname of the person
firstname,
//lastname of the person
lastname,

//the language can be'en' or 'es'
language


var person = {
// the firstname
firstname:'john'
}

console.log(person)