文章加密

;

2019年7月31日 星期三

原型, 模組, 閉包, 拉升, typeof, 不等性(Inequality)

https://cythilya.github.io/2018/10/10/intro-2/  看完後在緊急補一下ES6

原型(Prototype)( https://blog.techbridge.cc/2017/04/22/javascript-prototype/ )

原型可說是物件的一種 fallback 機制,當在此物件找不到指定屬性時,就會透過原型鏈結(prototype link / prototype reference)追溯到其父物件上。範例如下,若想存取 bar.a 但由於 bar 並無 a 屬性,因此就會透過原型鏈結找到 foo,並得到 100 這個值。
var foo = { a: 100 };

var bar = Object.create(foo); // 建立 bar 物件,並連結到 foo
bar.b = 'hi';

bar.a // 100,委派給 foo
bar.b // 'hi'
原型
另外,原型最常應用於「行為委派」(behavior delegation),如上例所示,將物件 bar 的行為委派給 foo,這也是常聽到很類似於其他語言的類別的繼承功能,但其實完全不同。


https://blog.techbridge.cc/2017/04/22/javascript-prototype/  該來理解 JavaScript 的原型鍊了

new运算符的缺点

用构造函数生成实例对象,有一个缺点,那就是无法共享属性和方法。

比如,在DOG对象的构造函数中,设置一个实例对象的共有属性species。

  function DOG(name){

    this.name = name;

    this.species = '犬科';

  }

然后,生成两个实例对象

  var dogA = new DOG('大毛');

  var dogB = new DOG('二毛');

这两个对象的species属性是独立的,修改其中一个,不会影响到另一个。

  dogA.species = '猫科';

  alert(dogB.species); // 显示"犬科",不受dogA的影响

每一个实例对象,都有自己的属性和方法的副本。这不仅无法做到数据共享,也是极大的资源浪费(站了兩個空間)。



prototype属性的引入

考虑到这一点,Brendan Eich决定为构造函数设置一个prototype属性

这个属性包含一个对象(以下简称"prototype对象"),所有实例对象需要共享的属性和方法,都放在这个对象里面;那些不需要共享的属性和方法,就放在构造函数里面。

实例对象一旦创建,将自动引用prototype对象的属性和方法。也就是说,实例对象的属性和方法,分成两种,一种是本地的,另一种是引用的。

还是以DOG构造函数为例,现在用prototype属性进行改写:

  function DOG(name){

    this.name = name;

  }

  DOG.prototype = { species : '犬科' };


  var dogA = new DOG('大毛');

  var dogB = new DOG('二毛');


  alert(dogA.species); // 犬科

  alert(dogB.species); // 犬科

现在,species属性放在prototype对象里,是两个实例对象共享的。只要修改了prototype对象,就会同时影响到两个实例对象。

  DOG.prototype.species = '猫科';


  alert(dogA.species); // 猫科

  alert(dogB.species); // 猫科
console.log(dogA.species === dogB.species) // false

P.S. 有些人會直接在 Array.prototype 上面加一些函式,讓自己可以更方便地做一些操作,原理也是這樣。可是一般來說,不推薦直接去修改不屬於你的 Object。
1
2
3
4
5
Array.prototype.last = function () {
    return this[this.length - 1];
};
  
console.log([1,2,3].last()) // 3

__proto__ (這個是要深入研究運行原理的,較複雜,而且不實用,可視情況跳過)

JavaScript 怎麼知道要到prototype去找?

function Person(name, age) {
  this.name = name;
  this.age = age;
}
  
Person.prototype.log = function () {
  console.log(this.name + ', age:' + this.age);
}
  
var nick = new Person('nick', 18);
  
console.log(nick.__proto__ === Person.prototype === Function.prototype) // true
//子.__proto__ === 父.prototype  (不能越級!!!)

// 那 Person.prototype.__proto__ 會指向誰呢?會指向 Object.prototype
console.log(Person.prototype.__proto__ === Object.prototype) // true
  
// 那 Object.prototype.__proto__ 又會指向誰呢?會指向 null,這就是原型鍊的頂端了
console.log(Object.prototype.__proto__) // null
nick 的__proto__會指向Person.prototype,所以在發現 nick 沒有 log 這個 method 的時候,JavaScript 就會試著透過__proto__找到Person.prototype,去看Person.prototype裡面有沒有 log 這個 method。
那假如Person.prototype還是沒有呢?那就繼續依照這個規則,去看Person.prototype.__proto__裡面有沒有 log 這個 method,就這樣一直不斷找下去。找到時候時候為止?找到某個東西的__proto__是 null 為止。意思就是這邊是最上層了。
而上面這一條透過__proto__不斷串起來的鍊,就叫做原型鍊。透過這一條原型鍊,就可以達成類似繼承的功能,可以呼叫自己 parent 的 method。

如果想知道一個屬性是存在 instance 身上,還是存在於它屬於的原型鍊當中,可以用hasOwnProperty這個方法:
1
2
3
4
5
6
7
8
9
10
11
12
function Person(name, age) {
  this.name = name;
  this.age = age;
}
  
Person.prototype.log = function () {
  console.log(this.name + ', age:' + this.age);
}
  
var nick = new Person('nick', 18);
console.log(nick.hasOwnProperty('log')); // false
console.log(nick.__proto__.hasOwnProperty('log')); // true
有了hasOwnProperty之後,我們就可以自己來模擬這段往上找的過程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function Person(name, age) {
  this.name = name;
  this.age = age;
}
  
Person.prototype.log = function () {
  console.log(this.name + ', age:' + this.age);
}
  
var nick = new Person('nick', 18);
  
function call(obj, methodName) {
  var realMethodOwner = obj;
  
  // 不斷往上找,直到 null 或者是找到真的擁有這個 method 的人為止
  while(realMethodOwner && !realMethodOwner.hasOwnProperty(methodName)) {
    realMethodOwner = realMethodOwner.__proto__;
  }
  
  // 找不到就丟一個 error,否則執行這個 method
  if (!realMethodOwner) {
    throw 'method not found.';
  } else {
    realMethodOwner[methodName].apply(obj);
  }
}
  
call(nick, 'log'); // nick, age:18
call(nick, 'not_exist'); // Uncaught method not found.

閉包(Closure)

閉包是指變數的生命週期只存在於該函式內,一旦離開了函式,該變數就會被回收而不可再利用,且必須在函式內事先宣告。
範例如下,在函式 closure 內可以存取 a 的值,但離開了函式 closure 走到全域範疇之下,就取不到 a 的值了,因此會被報錯「Uncaught ReferenceError: a is not defined」。
function closure() {
  var a = 1;
  console.log(a); // 1
}

closure();
a // Uncaught ReferenceError: a is not defined


模組(Module)

模組模式(Module Pattern)又稱為揭露模組(Revealing Module),經由建立一個模組實體(Module Instance,如下範例的 foo),來調用內層函式。而內層函式由於具有閉包的特性,因此可存取外層包含函式(Outer Enclosing Function)之內的變數和函式。透過模組模式,可隱藏私密的資訊,並對外公開 API。
範例如下,CoolModule 對外公開 API doSomething 和 doAnother,CoolModule 之外是無法取得其私有的 something 和 another 兩個變數的值。
function CoolModule() {
  var something = "cool";
  var another = [1, 2, 3];

  function doSomething() {
    console.log(something);
  }

  function doAnother() {
    console.log(another.join(" ! "));
  }

  return {
    doSomething: doSomething,
    doAnother: doAnother
  };
}

var foo = CoolModule();

foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

什麼是Module Pattern?解決什麼問題?

Module Pattern 利用函數的「閉包(closure)」特性來避免汙染全域的問題 - 使用閉包(closure)來提供封裝的功能,將方法和變數限制在一個範圍內存取與使用。這樣的好處除了避免汙染全域外,也將實作隱藏起來,只提供公開的介面(public API)供其他地方使用,簡單易懂。

物件實字(object literals)

第一個先來看物件實字(object literals)的觀念。
在object literal notation中,物件中的屬性和方法用這樣的方式被描述 - name/value pairs,即key/value的方式,並用分號(:)區隔 - 每個name/value pairs用逗號(,)區隔,而最後一個name/value pair後不需使用逗號結尾 - 使用大括號({})包裝起來
範例如下。
var myObjectLiteral = {
    variableKey: variableValue,
    functionKey: function(){
        // ...
    }
};
使用object literal的好處是將程式碼封裝和組織起來。
如果用new來建構也是可以的,但會發生一些非預期的結果,並不建議使用。
[Note] JavaScript並沒有如同我們熟知的其他語言(例如:Java、C#等)有private、protected和public這些語法可用,而要靠函數的作用域,即「閉包(closure)」來實作。

閉包(closure)

再來看閉包(closure)的觀念。
Closure是指變數的生命週期只存在於該function中,一旦離開了function,該變數就會被回收而不可再利用,且必須在function內事先宣告。
function closure() {
    var a = 1;
    console.log(a); //1
}
closure();
console.log(a); //Uncaught ReferenceError: a is not defined
對於closure進一步的探討可參考這篇文章 Closures

Module Pattern的範例

來看一個Module Pattern的實際範例。
var testModule = (function(){
    var counter = 0;
    return {
        incrementCounter: function(){
            return counter++;
        },
        resetCounter: function(){
            console.log('counter value prior to reset: ' + counter);
            counter = 0;
        }
    };
}());

//test
testModule.incrementCounter();
testModule.resetCounter(); //counter value prior to reset: 1
在這裡,變數counter是個private變數,無法被function外的其他地方任意存取,只能經由公開方法incrementCounter 和 resetCounter取用。 function最後會return一個物件,這個物件即是公開出去的API,讓程式的其他區域可以與之互動。 而這就是利用函數的閉包特性來達成的。
我們再看一個更複雜的例子...
var basketModel = (function(){
    //private
    var basket = [];
    function doSomethingPrivate(){
        //...
    }
    function doSomethingElsePrivate(){
        //...
    }

    //public
    return {
        addItem: function(value){
            basket.push(value);
        },
        getItemCount: function(){
            return basket.length;
        },
        doSomething: doSomethingPrivate(),
        getTotal: function(){
            var q = this.getItemCount(),
                p = 0;

            while(q--){
                p = p + basket[q].price;
            }
            return p;
        }
    }
}());

basketModel.addItem({
    item: 'bread',
    price: 0.5
});

basketModel.addItem({
    item: 'butter',
    price: 0.3
});

console.log(basketModel.getItemCount()); //2
console.log(basketModel.getTotal()); //0.8
我們這樣的存取是不行的...
console.log(basketModule.basket); //"basket"是private variable,不提供函數外部存取
console.log(basket); //"basket"在函數內部的時候才可以這樣呼叫
所以我們就可以為Module Pattern做一個範本,如下。
var myNamespace = (function(){
    //private members
    var myPrivateVariable = 0;
    var myPrivateMethod = function(someText){
        console.log(someText);
    };
    //public members
    return {
        myPublicVariable: 'foo',
        myPublicFunction: function(bar){
            myPrivateVariable++;
            myPrivateMethod(bar);
        }
    };
}());

console.log(myNamespace.myPublicVariable); //foo
myNamespace.myPublicFunction('hi'); //hi

優缺點?

優點是清楚的物件導向、封裝概念。
缺點是
  • 如果要變數或方法的public/private狀態,我們必須要去用到的每一個地方手動修改使用方式
  • 在debug時,對於private members較難偵測
  • private members難以擴充,彈性不高




嚴格模式(Strict Mode)

嚴格模式簡單說就是為了預防開發者的一些不小心或錯誤的行為,JavaScript 引擎協助做了一些檢測的工作,當開發者誤用時就把錯誤丟出來。可參考-MDN
範例如下,在未宣告變數而賦值的狀況下,會無預警的產生一個全域變數,但若使用嚴格模式('use strict')則會禁止這行為外,還會報錯,告知開發者變數尚未被定義。
'use strict';

a = 1; // Uncaught ReferenceError: a is not defined


巢狀範疇(Nested Scope)

若在目前執行的範疇找不到這個變數的時候,就會往外層的範疇搜尋,持續搜尋直到找到為止,或直到最外層的全域範疇(global scope,在瀏覽器底下就是指 window)。
如下,console.log(a + b) 中,b 無法在 foo 中找到,但可從全域範疇中追出來。
const foo = (a) => {
  console.log(a + b);
}

const b = 2;

foo(2); // 4


拉升(Hoisting)

在程式執行前,編譯器(compiler)會先由上到下逐行將程式碼轉為電腦可懂的命令,然後再執行編譯後的指令。在這個編譯的階段,編譯器找出所有的變數並繫結所屬範疇,但不賦值,所以此刻變數所帶的值是 undefined;而在執行階段,JavaScript 引擎才會處理給值的事情。
我們可以把這個過程想像成是將這些變數「提升」到程式碼的最頂端,如下範例所示,因此當印出 a 的值的時候,會是已宣告但還沒賦值的狀態,也就是有這個變數,但其值是 undefined,一直到程式執行了,才給值。因此,我們可以在程式碼任何地方呼叫運用這個變數,但只有在正式宣告之後才能有正確的值可用,在宣告之前使用都會得到 undefined。
var a; // 編譯時期的工作

console.log(a); // undefined
a = 2; // 執行時期的工作

不等性(Inequality)

關於不等性的比較運算子有 ><>=<= 共四種,在這裡有幾種狀況需要注意
  • 若比較的值都是字串,則以字典的字母順序為主。
'ab' < 'cd' // true
  • 若比較的值型別不同,由於值的不等性比較沒有嚴格不相等這種情況,因此,無論什麼樣的比較都會被強制轉型為數字,無法轉為數字的就會變成 NaN。
'99' > 98 // true,字串 '99' 被強制轉型為數字 99

'Hello World' > 1 // false,字串 'Hello World' 無法轉為數字,變成 NaN
'Hello World' < 1 // false
'Hello World' = 1 // false
  • NaN 不大於、不小於、不等於任何值,當然也不等於自己。
NaN > NaN // false
NaN < NaN // false
NaN === NaN // false
NaN == NaN // false

typeof

typeof 可用於檢測值的型別是什麼。
typeof 'Hello World!' // 'string'
typeof true // 'boolean'
typeof 1234567 // 'number'
typeof null // 'object'
typeof undefined // 'undefined'
typeof { name: 'Jack' } // 'object'
typeof Symbol() // 'symbol'
typeof function() {} // 'function'
typeof [1, 2, 3] // 'object'
typeof NaN // 'number'

不懂得都趕緊看一看
https://cythilya.github.io/archieve/

沒有留言:

張貼留言