JavaScript 高級知識技巧

對象

Js 共有number、string、boolean、null、undefined、object六種主要類型,除了object的其它五中類型都屬於基本類型,它們本身並不是對象。但是null有時會被當做對象處理,其原因在於不同的對象在底層都表示爲二進制,在 js 中二進制前三位都爲 0 的話就會被判定爲object類型,而null的二進制表示全是 0, 所以使用typeof操作符會返回object,而後續的 Js 版本爲了兼容前面埋下的坑,也就沒有修復這個 bug。

"I'm a string"本身是一個字面量,並且是一個不可變的值,如果要在這個字面量上執行一些操作,比如獲取長度、訪問某個字符等,那就需要將其轉換爲String類型,在必要的時候 js 會自動幫我們完成這種轉換,也就是說我們並不需要用new String('I'm a string')來顯示的創建一個對象。類似的像使用42.359.toFixed(2)時,引擎也會自動把數字轉換爲Number對象。

nullundefined沒有對應的構造形式,它們只有文字形式。相反,Date只有構造,沒有文字形式。對於Object、Array、FunctionRegExp(正則表達式)來說,無論使用文字形式還是構造形式,它們都是對象,不是字面量。

Array 類型

數組類型有一套更加結構化的值存儲機制,但是要記住的是,數組也是對象,所以有趣的是你也可以給數組添加屬性。

var myArray = ["foo", 42, "bar"];
myArray.baz = "baz";
myArray.length; // 3
myArray.baz; // "baz"

數組類型的length屬性是比較有特點的,它的特點在於不是隻讀的,也就是說你可以修改它的值。因此可以通過設置這個屬性從數組末尾刪除或添加新的項。

var colors = ["red", "blue", "green"];
colors.length = 2;
console.info(colors[2]); // undefined
colors.length = 4;
console.info(colors[4]); // undefined
// 向後面追加元素
colors[colors.length] = "black";

數組還有一些很方便的迭代方法,比如every()、filter()、forEach()、map()、some(),這些方法都不會修改數組中包含的值,傳入這些方法的函數會接收三個參數:數組項的值、該項在數組中的位置、和數組對象本身。

Function 類型

在 ECMAScript 中,每個函數都是Function類的實例,而且都與其它引用類型一樣具有屬性和方法。由於函數時對象,因此函數名實際上也是一個指向函數對象的指針,不會與某個函數綁定。

在函數的內部有兩個特殊的對象,thisargumentsarguments對象有calleecaller屬性。caller用來指向調用它的function對象,若直接在全局環境下調用,則會返回nullcallee用來指向當前執行函數,所以我們可以通過下面的方式來實現階乘函數。

function factorial(num) {
    if (num <= 1) {
        return 1;
    } else {
        return num * arguments.callee(num-1);
    }
}

每個函數都包含兩個非繼承而來的方法,apply()call(),這兩個方法都是在特定作用域中調用函數,實際上等於設置函數體內this對象的值。首先,apply()方法接收兩個參數,一個是在其中運行函數的作用域,另一個是參數數組,其中第二個參數可以是Array的實例,也可以是arguments對象。call()方法與apply()方法的作用相同,它們的區別僅僅在於接收參數的方式不同,在使用call()方法時必須逐個列舉出來。

window.color = "red";
var o = {color: "blue"};
function sayColor() {
    console.info(this.color);
}
sayColor(); // red
sayColor.call(this); // red
sayColor.call(window); // red
sayColor.call(o); // blue
sayColor.apply(o); // blue

需要注意的是,在嚴格模式下未指定環境對象而調用函數,則this值不會轉型爲window,除非明確把函數添加到某個對象或者調用apply()call()

安全的類型檢查

Js 內置的類型檢查機制並不是完全可靠的,比如在 Safari(第5版前),對正則表達式應用typeof操作符會返回function;像instanceof在存在多個全局作用域(包含 frame)的情況下,也會返回不可靠的結果;前文提到的 Js 一開始埋下的坑也會導致類型檢查出錯。

我們可以使用toString()方法來達到安全類型檢查的目的,在任何值上調用Object原生的toString()方法都會返回一個[object NativeConstructorName]格式的字符串,下面以檢查數組爲例。

Object.prototype.toString.call([]); // "[object Array]"
function isArray(val) {
    return Object.prototype.toString.call(val) == "[object Array]";
}

作用域安全的構造函數

構造函數其實就是一個使用new操作符調用的函數,當使用new操作符調用時,構造函數內用到的this對象會指向新創建的對象實例,比如我們有下面的構造函數。

function Person(name, age) {
    this.name = name;
    this.age = age;
}

現在的問題在於,要是我們不使用new操作符呢?會發生什麼!

let person = Person('name', 23);
console.info(window.name); // name
console.info(window.age); // 23

很明顯,這裏污染了全局作用域,原因就在於沒有使用new操作符調用構造函數,此時它就會被當作一個普通的函數被調用,this就被解析成了window對象。我們需要將構造函數修改爲先確認this是否是正確類型的實例,如果不是則創建新的實例並返回。

function Person(name, age) {
    if (this instanceof Person) {
        this.name = name;
        this.age = age;
    } else {
        return new Person(name, age);
    }
}

高級定時器

大部分人都知道使用setTimeout()setInterval()可以方便的創建定時任務,看起來好像 Js 也是多線程的一樣,實際上定時器僅僅是計劃代碼在未來的某個時間執行,但是執行時機是不能保證的。因爲在頁面的生命週期中,不同時間可能有其它代碼控制着 JavaScript 進程。

這裏需要注意一下setInterval()函數,僅當沒有該定時器的任何其他代碼實例時,Js 引起纔會將定時器代碼添加到隊列中。這樣可以避免定時器代碼可能在代碼再次被添加到隊列之前還沒有完成執行,進而導致定時器代碼連續運行好幾次的問題。但是這也導致了另外的問題:(1)某些間隔會被跳過;(2)多個定時器的代碼執行之間的間隔可能會比預期小。

假設某個click事件處理程序使用setInterval()設置了一個 200ms 間隔的重複定時器。如果這個事件處理程序花了 300ms 多的時間完成,同時定時器代碼也花了差不多了的時間,就會同時出現跳過間隔切連續運行定時器代碼的情況。

爲了避免setInterval()的重複定時器的這兩個缺點,我們可以使用如下模式的鏈式setTimeout(),代碼一看就懂什麼意思了。

setTimeout(function() {
    // 處理中
    setTimeout(arguements.callee, interval);
}, interval)

消息隊列與事件循環

如下圖所示,左邊的棧存儲的是同步任務,就是那些能立即執行、不耗時的任務,如變量和函數的初始化、事件的綁定等等那些不需要回調函數的操作都可歸爲這一類。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-S43TDTUd-1578672468656)(file://D:/workspace/blog/post-images/1570979103039.png)]

右邊的堆用來存儲聲明的變量、對象。下面的隊列就是消息隊列,一旦某個異步任務有了響應就會被推入隊列中。如用戶的點擊事件、瀏覽器收到服務的響應和setTimeout中待執行的事件,每個異步任務都和回調函數相關聯。
JS引擎線程用來執行棧中的同步任務,當所有同步任務執行完畢後,棧被清空,然後讀取消息隊列中的一個待處理任務,並把相關回調函數壓入棧中,單線程開始執行新的同步任務。

來看個例子:執行下面這段代碼,執行後,在 5s 內點擊兩下,過一段時間(> 5s)後,再點擊兩下,整個過程的輸出結果是什麼?

setTimeout(function(){
    for(var i = 0; i < 100000000; i++){}
    console.log('timer a');
}, 0)
for(var j = 0; j < 5; j++){
    console.log(j);
}
setTimeout(function(){
    console.log('timer b');
}, 0)
function waitFiveSeconds(){
    var now = (new Date()).getTime();
    while(((new Date()).getTime() - now) < 5000){}
    console.log('finished waiting');
}
document.addEventListener('click', function(){
    console.log('click');
})
console.log('click begin');
waitFiveSeconds();

首先,先執行同步任務。其中waitFiveSeconds是耗時操作,持續執行長達 5s。然後,在 Js 引擎線程執行的時候,'timer a'對應的定時器產生的回調、'timer b'對應的定時器產生的回調和兩次 click 對應的回調被先後放入消息隊列。由於 Js 引擎線程空閒後,會先查看是否有事件可執行,接着再處理其他異步任務,最後,5s 後的兩次 click 事件被放入消息隊列,由於此時 Js 引擎線程空閒,便被立即執行了。因此會產生下面的輸出順序。

0
1
2
3
4
click begin
finished waiting
click
click
timer a
timer b
click
click
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章