almost 2 years ago

開始學JS差不多半年多,從一開始以為JS只是用來調用DOM的Script language,到後來才發現他其實跟其他語言一樣都可以用來解決一般性的問題,接著學NodeJS、JQuery和框架Angular2與React,一路玩過來覺得很過癮,但也覺得自己對於JS語言核
心本身了解得不夠徹底,所以特別花一點時間了解JS最讓人混淆的地方。

JS基本型態

在JS中型態包含了Number、String、Boolean、Undefined、Date、Array、Object和Function

Object

宣告物件

參考文件MDN- Working_with_Objects,最基本的宣告物件共有四種方法

1.Function宣告
var o1  = function(){  
  this.name = name;
  this.yell = function(){
      console.log("Hello I am ", this.name);
  }  
};

2. Class宣告
class o2{
  constructor(name){
    this.name = name;
  }
  yell(){
    console.log("Hello I am ", this.name);
  }
}

//這兩種方式基本上是一樣的,只是class為ES6包裝的語法糖(90%一樣),如果要創建新的子物件可以透過

let p1 = new o1('Hello')
let p2 = new o2('World')

3. 透過Object直接創建物件
var o3 = new Object();
o3.name = "default";
o3.yell = function(){console.log("Hello I am ", this.name);}

4.物件實字 Object Literal
let o4 = { 'name':'default', 'yell':function(){console.log("Hello I am ", this.name);} }

//這兩種方法因為是物件,如果要用此創建新的子物件,必須使用Object.create(object)

let p3 = Object.create(o3)
let p4 = Object.create(o4)
物件特性設定

如果我們希望一個物件的屬性(attribute)不要被修改或是刪除等,可以透過一些方法設定該物件的特性
總共有三種

1.物件的屬性可以修改刪除,但不可以添加新的屬性
Object.preventExtensions(o1);
Object.isExtensible(o1); //檢查物件是否為extensible,回傳真偽值

o1.gender = 'boy'; // 原本o1沒有gender這個屬性,此時不會報錯

console.log(o1.gender) // undefined!


2.物件屬性只能修改,不可刪除與擴展
Object.seal(o1);
Object.isSeal(o1);
o1.name = "blabla" //可以修改

delete o1.name // 回傳false,不給刪除

o1.gender = 'boy'; // 原本o1沒有gender這個屬性,此時不會報錯

console.log(o1.gender) // undefined!


3.只能查詢
Object.freeze(o1);
Object.isFrozen(o1);
o1.name = "XXX"; //值沒有改變,但也不會報錯

o1.gender = 'boy'; // 原本o1沒有gender這個屬性,此時不會報錯

console.log(o1.gender) // undefined!
物件屬性(property)與物件屬性特性(attribute)設定

參考文件MDN - Define Property,剛才是設定物件本身的特性,針對物件個別屬性也可以做個詳細的特性設定

//最基本的屬性宣告

o1.name = "XXX";
o1["name"] = "XXX";

//定義屬性特性

Object.defineProperty(o1, 'gender', {
    __proto__: null,     // 避免繼承屬性

    'value':,            //預設為undefined

  'writable':true,     //預設為false

  'enumerable':true,   //預設為false

  'configurable':true, //預設為false

  'get':function(){},  //預設為undefined

  'set':function(){}   //預設為undefined

})

writablevalue可以可以被改變
enumerable:屬性可不可以枚舉(enumeration)
configurable:屬性可不可以再設定
getter/settervalue就不用多解釋了

//實際應用的範例節錄自MDN

var bValue = 38;
Object.defineProperty(o, 'b', {
  get: function() { return bValue; },
  set: function(newValue) { bValue = newValue; },
  enumerable: true,
  configurable: true
});
o.b; // 38


var o = {};
Object.defineProperty(o, 'a', {
  get: function() { return 1; },
  configurable: false
});
Object.defineProperty(o, 'a', { configurable: true }); // throws a TypeError

透過上述的方法,可以用於設定物件的private變數,以下範例節錄自MDN

function Archiver() {
  var temperature = null;
  var archive = [];

  Object.defineProperty(this, 'temperature', {
    get: function() {
      console.log('get!');
      return temperature;
    },
    set: function(value) {
      temperature = value;
      archive.push({ val: temperature });
    }
  });

  this.getArchive = function() { return archive; };
}

var arc = new Archiver();
arc.temperature; // 'get!'

arc.temperature = 11;
arc.temperature = 13;
arc.getArchive(); // [{ val: 11 }, { val: 13 }]
物件的Prototype、Method與Prototype Chain

先前介紹的物件屬性,不論是變數或是靜態方法,都是屬於個別物件,也就是說每次使用new 的新物件都會配置新的記憶體空間。
而在JS的物件中,他並不像是C++/Java這類以class為導向的物件,JS則是prototype導向,所有的物件的prototype都繼承自父類,一路指向Object,最後指向null;null沒有prototype,也是prototype chain的終點。
物件的prototype與靜態方法的兩大差別:prototype的變數與函式都是共用記憶體的;靜態方法可用於靜態物件上而prototype的函式必須用在實例化的物件上(instance of object)

class Person {
    constructor(name){
    this.name = name;
  }
  
  static whoAreYou(p){
    console.log("You are ", p.name);
  }
  
  sayHello(){
    console.log("Hello I am ", this.name)
  }
}

let p = new Person("Wang");
Person.whoAreYou(p);
console.log(p);
// 每當使用new,首先會產生新物件的prototype

// 此時p的prototype chain: p --> Person --> Object --> Null

//當物件呼叫屬性時,他會先從物件本身屬性開始找,接著找prototype的屬性,接著找prototype的prototype...一路沿著prototype chain查找直到null


class Student extends Person {
    constructor(id, name){
    super(name);
    this.id = id;
  }
  //複寫父類函式

  sayHello(){
    console.log("Hello I am ", this.name , " and I am a student");
  }
}

let s = new Student("Yan", 2);

//因為可以透過prototype chain查找,所以可以事後添加新的prototype屬性

Studenet.prototype.sayId = function(){console.log("this is My ID ", this.id)}
s.sayId(); //函式成功!

Function

參考文章:understanding-scope-and-context-in-javascript,在JS中,最數Function的觀念最讓人混淆,其中有兩大核心必須釐清scope和context

scope是指在function中變數解析的方式;而context專指哪個物件執行該function,也就是this的reference到底是誰
兩者基本上是不相干的東西

在JS中,變數的scope不像大多數的語言是block-base,一個for迴圈或是if判斷式內可以擁有自己的區域變數;JS是function-base的scope,直到ES6提出let/const才有了block-base的變數scope。
在runtime時,每當JS intepreter解析到一個Function時,就會產生Execution Context並push到execution stack上,在每個Execution Context中又分成 creation/execution phase

creation中主要是產生variable object (又稱activation object)為每個變數、子函式和參數配置記憶體,接著初始化scope chain並決定this
execution則進行解析與執行程式

切記,這裡的Execution Context和Context是不同的觀念!不要被名稱給混淆了。
每個函式都有自己的scope chain,這決定了之後函式的變數存取方式!這部分跟剛才的物件 prototype chain很類似。
圖解的部分可以參考之前我寫的另一篇影片心得JS-VM internals, EventLoop, Async and ScopeChains 影片筆記

function A(){
    let name = "hello";
  console.log(this,name);
  function B(){
    let id = 1;
    console.log(this,name, id);
  }
  return B;
}
//打印出來, this都是指向window

let a = A();
a();

//使用new,第一個this會指向新的物件,也就是A

let a = new A();
//此時執行的context回到window上!

b();

所以最經典的closure閉包問題為何打印出來都是10,原因就是因為function在存取變數時如果找不到,就會往上一層的scope chain找,此時透過的是reference,這也是為什麼要在用一層函式包住轉為local varable解決問題。

for(var i=0; i<10; i++){
    setTimeout(()=>{console.log(i)},1000)
}

for(var i=0; i<10; i++){
    ((v)=>{setTimeout(()=>{console.log(v)},1000)})(i)
}

//改成let也可以解決此問題

for(let i=0; i<10; i++){
    setTimeout(()=>{console.log(i)},1000)
}
IIFE(immediately-invoked function expression)

自我呼叫函式也是利用closure的特性,主要用來避免變數全域污染,常用於各大library和framework,像是jQuery、lodash等

(function(window) {
    var foo, bar;
    function private() {
        // do something

    }
    window.Module = {
        public: function() {
            // do something 

        }
    };
})(this);
Context、Call、Apply和Bind

物件執行的Context基本上就是物件本身,此外可以透過Call和Apply綁定新的Context;而Bind是ES5提出新的綁定Context方式,會回傳一個新的函式。
在ES6中的arrow funciton ()=>{},會直接綁定當前的Context作為執行時的Context。
常應用的場景在於物件函式的callback function上,因為此時的context基本上已經移轉到window上,如果不重新綁定的話this就會指向全域。

結論

弄清楚Object的Property、Prototype、Static Methods以及Prototype Chain 和 Funtion的Scope Chain、Context以及執行中的Execution Context,才有辦法更近一步理解JS的Design Pattern與各大Framework的實作內容。

← D3 - 深入了解Selection與Data綁定 關於前端測試與Selenium實戰(Firefox/Chrome/Android)-使用Docker與Nodejs →
 
comments powered by Disqus