【考前必看一】面試前夕知識點梳理之JavaScript(二)

【JavaScript】

一、構造函數

  • 構造函數是一種特殊的方法,主要用來創建對象時初始化對象,總與new運算符一起使用。

二、new運算符

  • new運算符會創建一個空對象,並且構造函數中的 this 指向這個空對象
  • 這個新對象會被執行[[原型]]連接,即連接構造函數的原型
  • 執行構造函數,將屬性和方法添加到 this 引用的對象中,即創建的這個新對象
  • 如果構造函數中沒有返回其它對象,那麼返回 this,即創建的這個新對象;否則,返回構造函數中返回的對象
function _new() {
    let target = {}; // 創建的新對象
    // 第一個參數是構造函數
    let [constructor, ...args] = [...arguments];
    // 執行 [[原型]] 連接 ;target 是 constructor 的實例
    target.__proto__ = constructor.prototype;
    // 執行構造函數,將屬性或方法添加到創建的空對象上
    let result = constructor.apply(target, args);
    if (result && (typeof (result) == "object" || typeof (result) == "function")) {
        // 如果構造函數執行的結構返回的是一個對象,那麼返回這個對象
        return result;
    }
    // 如果構造函數返回的不是一個對象,返回創建的新對象
    return target;
}

三、原型和原型鏈

在JavaScript中,每個對象都可以稱之爲原型。原型的值可以是一個對象,也可以是null

  • 原型對象都包含一個指向構造函數的指針,同時構造函數也有一個內部屬性prototype,這個內部屬性是一個指針,指向該原型對象。注:有且僅有函數纔有prototype屬性。
  • 通過構造函數可以新建原型對象的實例,所有的對象實例都有一個內部屬性[[prototype]],這個內部屬性是一個指針,指向原型對象。注:null 沒有內部屬性[[prototype]]。
  • 原型對象的作用是可以讓所有的對象實例共享它所包含的屬性方法

如果原型的值是一個對象,那麼這個對象也一定有自己的原型。這樣就形成了一條線性的鏈,我們稱之爲原型鏈

  • 原型鏈的盡頭是 null,即一個空對象
  • 原型鏈的作用是用來實現繼承。比如我們新建一個數組,數組的方法就是從數組的原型上繼承而來的。

四、繼承

ES5 一共有有六種方式可以實現繼承,分別爲:原型鏈繼承、借用構造函數、組合繼承 (原型鏈 + 借用構造函數)、原型式繼承、寄生式繼承、寄生組合式繼承。

第一種方式:原型鏈繼承

  • 原型鏈繼承的基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法
function SuperType() {
    this.name = 'Yvette';
    this.colors = ['pink', 'blue', 'green'];
}
SuperType.prototype.getName = function () {
    return this.name;
}
function SubType() {
    this.age = 22;
}

// 繼承
SubType.prototype = new SuperType();
SubType.prototype.getAge = function() {
    return this.age;
}
// SubType.prototype.constructor = SubType;
// 如果不寫上面這句, SubType 的原型的 constructor 則爲 SuperType

let instance1 = new SubType();
instance1.colors.push('yellow');
console.log(instance1.getName()); //'Yvette'
instance1.name = 'Jack';
console.log(instance1.getName()); //'Jack'
console.log(instance1.colors);//[ 'pink', 'blue', 'green', 'yellow' ]
 
let instance2 = new SubType();
console.log(instance2.getName()); //'Yvette'
console.log(instance2.colors);//[ 'pink', 'blue', 'green', 'yellow' ]

1. 別忘記默認的原型

2. 確定原型和實例的關係:

  • instanceof:某個構造函數是否出現在實例對象的原型鏈上
instance1 instanceof Object; // true
instance1 instanceof SuperType; // true
instance1 instanceof SubType; // true
  • isPrototypeOf():某個實例對象是否是原型 派生的實例
Object.prototype.isPrototypeOf(instance1); // true
SuperType.prototype.isPrototypeOf(instance1); // true
SubType.prototype.isPrototypeOf(instance1); // true

3. 謹慎地定義方法:

子類型重寫超類型的某個方法,或者需要添加超類型中不存在的某個方法。不管怎樣,給原型添加方法的代碼一定要放在替換原型的語句之後。其中,子類型重寫超類型的某個方法,並不會改變由超類型構造函數生成的實例對象的結果,該實例對象調用的仍然是原來的方法。

function SuperType() {
    this.name = 'Yvette';
    this.colors = ['pink', 'blue', 'green'];
}
SuperType.prototype.getName = function () {
    return this.name;
}
function SubType() {
    this.age = 22;
}
// 繼承
SubType.prototype = new SuperType();
// 定義方法
SubType.prototype.getAge = function() {
    return this.age;
}
// 重寫方法
SubType.prototype.getName = function() {
    return 'Alice';
}

// 通過 SuperType 生成的實例
let instance = new SuperType();
console.log(instance.getName()); // 'Yvette'

// 通過 SubType 生成的實例 
let instance1 = new SubType();
console.log(instance1.getName()); // 'Alice'

不能使用對象字面量創建原型方法,否則會切斷 SubType 和 SuperType 的關係。 

function SuperType() {
    this.name = 'Yvette';
    this.colors = ['pink', 'blue', 'green'];
}
SuperType.prototype.getName = function () {
    return this.name;
}
function SubType() {
    this.age = 22;
}

// 繼承
SubType.prototype = new SuperType();

// 使用字面量添加新方法,會導致上一行代碼失效
SubType.prototype =  {
    getSub:function(){},
    getSup:function(){}
}

let instance1 = new SubType();
console.log(instance1.getName()); // error!

4. 原型鏈的問題:

  • 通過原型鏈實現繼承時,原型會變成另一個原型的實例,原先的實例屬性變成了現在的原型屬性,該原型的引用類型屬性會被所有的實例共享
  • 在創建子類型的實例時,沒有辦法在不影響所有對象實例的情況下,給超類型的構造函數傳遞參數。

 

第二種方式:借用構造函數

  • 借用構造函數的基本思想是在子類型的構造函數中調用超類型的構造函數
function SuperType(name) {
    this.name = name;
    this.colors = ['pink', 'blue', 'green'];
}
function SubType(name) {
    SuperType.call(this, name);
}
let instance1 = new SubType('Yvette');
instance1.colors.push('yellow');
console.log(instance1.colors);//['pink', 'blue', 'green', yellow]

let instance2 = new SubType('Jack');
console.log(instance2.colors); //['pink', 'blue', 'green']

優點:

  • 能夠向超類型的構造函數傳遞參數。
  • 解決了原型中包含引用類型值被所有實例共享的問題。

缺點:

  • 方法都在構造函數中定義,函數複用無從談起。
  • 超類型原型中定義的方法對於子類型而言都是不可見的。

第三種方式:組合繼承 (原型鏈 + 借用構造函數) —— 最常用的繼承模式

  • 組合繼承的基本思想是使用原型鏈實現對原型屬性和方法的繼承,通過借用構造函數來實現對實例屬性的繼承。既通過在原型上定義方法來實現了函數複用,又保證了每個實例都有自己的屬性。
function SuperType(name) {
    this.name = name;
    this.colors = ['pink', 'blue', 'green'];
}
SuperType.prototype.sayName = function () {
    console.log(this.name);
}
function SuberType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}
SuberType.prototype = new SuperType();
SuberType.prototype.constructor = SuberType;
SuberType.prototype.sayAge = function () {
    console.log(this.age);
}
let instance1 = new SuberType('Yvette', 20);
instance1.colors.push('yellow');
console.log(instance1.colors); //[ 'pink', 'blue', 'green', 'yellow' ]
instance1.sayName(); //Yvette

let instance2 = new SuberType('Jack', 22);
console.log(instance2.colors); //[ 'pink', 'blue', 'green' ]
instance2.sayName();//Jack

優點:

  • 能夠向超類型的構造函數傳遞參數。
  • 每個實例都有自己的屬性。
  • 實現了函數複用。

缺點:

  • 無論什麼情況下,都會調用兩次超類型構造函數:一次是在創建子類型原型的時候,另一次是在子類型構造函數內部。

第四種方式:原型式繼承

  • 原型式繼承的基本思想是藉助原型可以基於已有的對象創建新對象,同時還不必因此創建自定義類型
function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

在 object() 函數內部,先創建了一個臨時性的構造函數,然後將傳入的對象作爲這個構造函數的原型,最後返回了這個臨時類型的一個新實例。從本質上講,object() 對傳入的對象執行了一次淺拷貝。 

ECMAScript5 通過新增 Object.create() 方法 規範了原型式繼承。這個方法接收兩個參數:一個用作新對象原型的對象和(可選的)一個爲新對象定義額外屬性的對象 (可以覆蓋原型對象上的同名屬性),在傳入一個參數的情況下,Object.create() 和 object() 方法的行爲相同

var person = {
    name: 'Yvette',
    hobbies: ['reading', 'photography']
}
var person1 = Object.create(person);
person1.name = 'Jack';
person1.hobbies.push('coding');
var person2 = Object.create(person);
person2.name = 'Echo';
person2.hobbies.push('running');
console.log(person.hobbies);//[ 'reading', 'photography', 'coding', 'running' ]
console.log(person1.hobbies);//[ 'reading', 'photography', 'coding', 'running' ]
// 向 Object.create() 方法中傳入兩個參數
var person = {
    name: 'Yvette',
    hobbies: ['reading', 'photography']
}
var antherPerson = Object.create(person,{
    name:{
        value:'Jack'
    }
});
console.log(antherPerson.name); // 'Jack'

在沒有必要創建構造函數,僅讓一個對象與另一個對象保持相似的情況下,原型式繼承是可以勝任的。

缺點:

  • 同原型鏈繼承一樣,包含引用類型值的屬性會被所有實例共享。

第五種方式:寄生式繼承

  • 寄生式繼承是與原型式繼承緊密相關的一種思路。寄生式繼承的思路與寄生構造函數和工廠模式類似,即創建一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來增強對象,最後再像真地是它做了所有工作一樣返回對象
function createAnother(original) {
    // 通過調用函數創建一個新對象, object()函數不是必須的, 任何能夠返回新對象的函數都適用於此模式
    var clone = object(original); 

    // 以某種方式增強這個對象
    clone.sayHi = function () {  
        console.log('hi');
    };

    // 返回這個對象
    return clone;
}
var person = {
    name: 'Yvette',
    hobbies: ['reading', 'photography']
};

var person2 = createAnother(person);
person2.sayHi(); // hi

基於 person 返回了一個新對象 —— person2,新對象不僅具有 person 的所有屬性和方法,而且還有自己的 sayHi() 方法。在考慮對象而不是自定義類型和構造函數的情況下,寄生式繼承也是一種有用的模式。

缺點:

  • 使用寄生式繼承來爲對象添加函數,會由於不能做到函數複用而效率低下。
  • 同原型鏈繼承一樣,包含引用類型值的屬性會被所有實例共享。

第六種方式:寄生組合式繼承 —— 最理想的繼承範式

  • 寄生組合式繼承,即通過借用構造函數來繼承屬性,通過原型鏈的混成形式來繼承方法
  • 寄生組合式繼承的基本思想是不必爲了指定子類型的原型而調用超類型的構造函數,我們需要的僅是超類型原型的一個副本,本質上就是使用寄生式繼承來繼承超類型的原型,然後再將結果指定給子類型的原型。

寄生組合式繼承的基本模式如下所示:

function inheritPrototype(subType, superType) {
    var prototype = object(superType.prototype); // 創建對象
    prototype.constructor = subType; // 增強對象
    subType.prototype = prototype; // 指定對象
}
  • 第一步:創建超類型原型的一個副本。

  • 第二步:爲創建的副本添加 constructor 屬性。

  • 第三步:將新創建的對象賦值給子類型的原型。

至此,我們就可以通過調用 inheritPrototype 來替換爲子類型原型賦值的語句:

function SuperType(name) {
    this.name = name;
    this.colors = ['pink', 'blue', 'green'];
}
SuperType.prototype.sayName = function(){
    return this.name;
}

function SuberType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}

// 重點
inheritPrototype(SuberType, SuperType);

SuberType.prototype.sayAge = function(){
    return this.age;
}

優點:

  • 只調用了一次超類構造函數,效率更高,避免在SuberType.prototype上面創建不必要的、多餘的屬性,與此同時,原型鏈還能保持不變。因此寄生組合式繼承是引用類型最理性的繼承範式。

五、作用域和作用域鏈

1. 作用域 分爲全局作用域函數作用域塊級作用域

2. 作用域鏈 就是從當前作用域開始一層一層向上尋找某個變量,直到找到全局作用域還是沒找到,就宣佈放棄。這種一層一層的關係,就是作用域鏈。例如:

let a = 10;
function fn1() {
    let b = 20;
    function fn2() {
        a = 20
    };
    return fn2;
}
fn1()();

fn2 作用域鏈 = [ fn2 作用域,fn1 作用域,全局作用域 ]

六、this對象

1. this 是函數運行時,在函數體內部自動生成的一個對象,只能在函數體內部使用

2. this 的指向:誰調用它,this 就指向誰。分爲三種情況:

  • 純粹的函數調用:如果是一般的函數調用,this指向全局對象;在嚴格模式"use strict"下,爲undefined。
  • 作爲對象方法的調用:如果是在對象的方法裏調用,this指向調用該方法的對象。
  • 作爲構造函數調用:如果是在構造函數裏調用,this指向創建出來的實例對象。

3. 改變this指向的四種方法:

  • 使用 that 
  • 使用.bind():Object.bind(this,obj1,obj2,obj3) 。
  • 使用.apply():Object.apply(this,arguments) 。
  • 使用.call():Object.call(this,obj1,obj2,obj3) 。

注意:bind()方法只會返回一個函數,並不會執行函數,而 apply() 和 call() 會立即執行函數。

4. 箭頭函數的情況:箭頭函數沒有自己的 this,繼承外層上下文綁定的 this。

let obj = {
    age: 20,
    info: function() {
        return () => {
            console.log(this.age); //this 繼承的是外層上下文綁定的 this
        }
    }
}

let person = {age: 28};
let info = obj.info();
info(); //20

let info2 = obj.info.call(person);
info2(); //28

七、閉包

1. 閉包的定義:閉包是指有權訪問另一個函數作用域中的變量的函數。

2. 創建一個閉包:閉包使得函數可以繼續訪問定義時的詞法作用域。拜 fn 所賜,在 foo() 執行後,foo 內部作用域不會被銷燬。 

function foo() {
    var a = 2;
    return function fn() {
        console.log(a);
    }
}
let func = foo();
func(); // 輸出 2

3. 閉包的作用

  • 能夠訪問函數定義時所在的詞法作用域 (阻止其被回收)。
  • 私有化變量
function base() {
    let x = 10; // 私有變量
    return {
        getX: function() {
            return x;
        }
    }
}
let obj = base();
console.log(obj.getX()); //10
  • 模擬塊級作用域
var a = [];
for (var i = 0; i < 10; i++) {
    a[i] = (function(j){
        return function () {
            console.log(j);
        }
    })(i);
}
a[6](); // 6
  • 創建模塊
function coolModule() {
    let name = 'Yvette';
    let age = 20;
    function sayName() {
        console.log(name);
    }
    function sayAge() {
        console.log(age);
    }
    return {
        sayName,
        sayAge
    }
}
let info = coolModule();
info.sayName(); //'Yvette'

模塊模式具有兩個必備的條件 (來自《你不知道的 JavaScript》)

  • 必須有外部的封閉函數,該函數必須至少被調用一次 (每次調用都會創建一個新的模塊實例)。

  • 封閉函數必須返回至少 一個 內部函數,這樣內部函數才能在私有作用域中形成閉包,並且可以訪問或者修改私有的狀態。 

八、同步和異步 以及 JS的執行機制

1. 同步可以理解爲在執行完一個函數或方法之後,一直等待系統返回值或消息,這時程序是出於阻塞的,只有接收到返回的值或消息後才往下執行其他的命令。

2. 異步執行完函數或方法後,不必阻塞性地等待返回值或消息,只需要向系統委託一個異步過程,那麼當系統接收到返回值或消息時,系統會自動觸發委託的異步過程,從而完成一個完整的流程。(當一個異步過程調用發出後,調用者不能立刻得到結果;實際處理這個調用的部件在完成後,通過狀態、通知和回調來通知調用者)

3. 異步編程的方法:回調函數、事件監聽、發佈/訂閱、Promises對象。

  • 方法1:回調函數(callbacks)。優點是簡單、容易理解和部署。缺點是不利於代碼的閱讀和維護,各個部分之間高度耦合(Coupling),流程會很混亂,而且每個任務只能指定一個回調函數。
  • 方法2:事件監聽。可以綁定多個事件,每個事件可以指定多個回調函數,而且可以“去耦合”(Decoupling),有利於實現模塊化。缺點是整個程序都要變成事件驅動型,運行流程會變得很不清晰。
  • 方法3:發佈/訂閱。性質與“事件監聽”類似,但是明顯優於後者。
  • 方法4:Promises對象。它是CommonJS工作組提出的一種規範,目的是爲異步編程提供統一接口。簡單說,它的思想是,每一個異步任務返回一個Promise對象,該對象有一個then方法,允許指定回調函數。

4. 異步加載 JS 腳本的方式:async 或者 defer、動態創建 script 標籤、XHR 異步加載 JS。

  • 方法1:async 或者 defer。<script> 標籤中增加 async (html5) 或者 defer (html4) 屬性,腳本就會異步加載。
<script src="../XXX.js" async></script>
<script src="../XXX.js" defer></script>

     defer 和 async 的區別在於:

  1. defer 要等到整個頁面在內存中正常渲染結束(DOM 結構完全生成,以及其他腳本執行完成),在 window.onload 之前執行;而 async 一旦下載完,渲染引擎就會中斷渲染,執行這個腳本以後,再繼續渲染。

  2. 如果有多個 defer 腳本,會按照它們在頁面出現的順序加載;而多個 async 腳本不能保證加載順序。

  • 方法2:動態創建 script 標籤。動態創建的 script ,設置 src 並不會開始下載,而是要添加到文檔中,JS 文件纔會開始下載。
let script = document.createElement('script');
script.src = 'XXX.js';
// 添加到 html 文件中才會開始下載
document.body.append(script);
  • 方法3:XHR 異步加載 JS。
let xhr = new XMLHttpRequest();
xhr.open("get", "js/xxx.js", true);
xhr.send();
xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status == 200) {
        eval(xhr.responseText);
    }
}

5. JS的執行機制:

除了廣義的同步任務和異步任務,我們對任務有更精細的定義:

  • macro-task (宏任務):整體代碼script,setTimeout,setInterval
  • micro-task (微任務):Promise,process.nextTick

事件循環流程:

  • 執行整體代碼script,作爲第一個宏任務,進入主線程。注意:遇到 setTimeout、setInterval,將其回調函數分發到宏任務Event Queue中。遇到 Promise,new Promise會立即執行一次,然後將 then 分發到微任務Event Queue中。遇到process.nextTick,將其回調函數分發到微任務Event Queue中。
  • 執行微任務Event Queue:Promise、process.nextTick
  • 執行宏任務Event Queue:setTimeout、setInterval

事件循環,宏任務,微任務的關係如圖所示:

console.log('1');
 
setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})
 
setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

// 完整的輸出爲1,7,6,8,2,4,3,5,9,11,10,12。

九、深拷貝和淺拷貝

1. 基本數據類型不存在深淺拷貝,兩者相互獨立,互不影響。

當 var a = 1,var b = a 時,相當於在棧內存中新開闢了一個內存,如下所示:

(2) 深拷貝和淺拷貝是針對複雜的引用數據類型(對象類型)來說的,淺拷貝只拷貝一層,而深拷貝是層層拷貝。

2. 淺拷貝:淺拷貝是會將對象的每個屬性進行依次複製,但是當對象的屬性值是引用類型時,實質複製的是其引用,當引用指向的值改變時也會跟着變化。

當 var a = [0,1,2,3,4],var b = a 時,a 和 b 會指向同一個堆地址,所以當我們改變 a 時 b 也會改變,改變 b 時 a 也會改變,如下所示:

3. 深拷貝:深拷貝複製變量值,對於非基本類型的變量,則遞歸至基本類型變量後,再複製。深拷貝後的對象與原來的對象是完全隔離的,互不影響,對一個對象的修改並不會影響另一個對象。

在深拷貝的情況下,我們會爲 b 單獨開闢一塊堆內存,如下所示:

深拷貝的實現:

  • 深拷貝最簡單的實現是: JSON.parse(JSON.stringify(obj)),這是最簡單的實現方式。但是有一些缺陷:
  1. 對象的屬性值是函數時,無法拷貝。
  2. 原型鏈上的屬性無法拷貝。
  3. 不能正確的處理 Date 類型的數據。
  4. 不能處理 RegExp。
  5. 會忽略 symbol。
  6. 會忽略 undefined。
  • 實現一個 deepClone 函數。
  1. 如果是基本數據類型,直接返回。
  2. 如果是 RegExp 或者 Date 類型,返回對應類型。
  3. 如果是複雜數據類型,遞歸。
  4. 考慮循環引用的問題。
function deepClone(obj, hash = new WeakMap()) { // 遞歸拷貝
    if (obj instanceof RegExp) return new RegExp(obj);
    if (obj instanceof Date) return new Date(obj);
    if (obj === null || typeof obj !== 'object') {
        // 如果不是複雜數據類型,直接返回
        return obj;
    }
    if (hash.has(obj)) {
        return hash.get(obj);
    }
    /**
     * 如果 obj 是數組,那麼 obj.constructor 是 [Function: Array]
     * 如果 obj 是對象,那麼 obj.constructor 是 [Function: Object]
     */
    let t = new obj.constructor();
    hash.set(obj, t);
    for (let key in obj) {
        // 遞歸
        if (obj.hasOwnProperty(key)) {// 是否是自身的屬性
            t[key] = deepClone(obj[key], hash);
        }
    }
    return t;
}

十、事件流、事件冒泡 、事件捕獲、事件委託

1. 事件流:從頁面中接收事件的順序。也就是說當一個事件產生時,這個事件的傳播過程,就是事件流。

2. IE的事件流叫做事件冒泡。事件冒泡:事件開始時由最具體的元素(文檔中嵌套層次最深的那個節點)接收,然後逐級向上傳播到較爲不具體的節點(文檔)。對於HTML來說,就是當一個元素產生了一個事件,它會把這個事件傳遞給它的父元素,父元素接收到了之後,還要繼續傳遞給它的上一級元素,就這樣一直傳播到document對象(現在的瀏覽器到window對象,只有IE8及以下不這樣)。

  • 阻止事件冒泡的方法:如果是 IE 瀏覽器,則使用 window.event.cancelBubble = true;如果是非 IE 瀏覽器,則使用 e.stopPropagation() 。stopPropagation() 是事件對象(Event)的一個方法,作用是阻止目標元素的冒泡事件,但是不會取消默認行爲。
// 阻止冒泡事件
function stopBubble(e){
    // 如果提供了事件對象,則這是一個非IE瀏覽器
    if(e && e.stopPropagation){
        e.stopPropagation();
    } else {
        // 否則,使用IE的方式來取消事件冒泡
        window.event.cancelBubble = true;
    }
}
  • 取消默認行爲的方法:如果是 IE 瀏覽器,則使用 window.event.returnValue = false;如果是非 IE 瀏覽器,則使用 e.preventDefault()。preventDefault() 是事件對象(Event)的一個方法,作用是取消目標元素的默認行爲。既然是說默認行爲,當然是元素必須有默認行爲才能被取消,如果元素本身就沒有默認行爲,調用當然就無效了。
// 取消默認行爲
function stopDefault(e){
    if(e && e.preventDefault){
        e.preventDefault();
    } else {
        window.event.returnValue = false;
    }  
    return false;  
}

注意:原生 JavaScript 的 return false 只會取消默認行爲;但如果使用的是 jQuery 的話,則既會取消默認行爲又可以防止對象冒泡。

3. 事件捕獲 的思想是不太具體的元素應該更早接受到事件,而最具體的節點應該最後接收到事件。事件捕獲的用意在於在事件到達預定目標之前捕獲它。即和冒泡的過程正好相反。以HTML的click事件爲例,document對象(DOM級規範要求從document開始傳播,但是現在的瀏覽器是從window對象開始的)最先接收到click事件,然後事件沿着DOM樹依次向下傳播,一直傳播到事件的實際目標。

4. 事件委託:利用了事件冒泡,只指定一個事件處理程序,就可以管理某一類型的所有事件,即把事件加到父級上,觸發執行效果。

  • 事件委託的好處:1)大大減少了DOM操作(相比於要獲取每一個子元素,使用事件委託,只需獲取一個DOM元素—父元素);2)佔用更少的內存空間(函數也是對象,都會佔用內存。相比於爲每一個子元素添加事件,使用事件委託,只需爲父元素添加一個事件處理程序);3)新添加的元素還會有之前的事件。最終,提高了頁面性能。
  • 適用事件委託的事件:多數鼠標事件和鍵盤事件,如 click,mousedown,mouseup,keydown,keyup,keypress。雖然 mouseover 和 mouseout 事件也冒泡,但要適當地處理他們並不容易,而且經常需要計算元素的位置。(因爲當鼠標從一個元素移到其子節點時,或者當鼠標移出該元素時,都會觸發 mouseout 事件。)
  • 實例:在JavaScript中,DOM0級、DOM2級與舊版本IE(8-)爲對象添加事件的方法不同,爲了以跨瀏覽器的方式處理事件,需要編寫一段“通用代碼”,即跨瀏覽器的事件處理程序,習慣上,這個方法屬於一個名爲 EventUtil 的對象,編寫並使用該對象後,可保證處理事件的代碼能在大多數瀏覽器下一致的運行。注意:EventUtil 對象是需要自己封裝的。
    詳情見:https://blog.csdn.net/Dora_5537/article/details/102003251
// HTML 代碼
<ul id='myLinks'>
    <li id="goSomewhere">Go somewhere</li>
    <li id="doSomething">Do something</li>
    <li id="sayHi">Say hi</li>
</ul>
// 爲每一個子元素添加事件
var item1 = document.getElementById("goSomewhere");
var item2 = document.getElementById("doSomething");
var item3 = document.getElementById("sayHi");

EventUtil.addHandler(item1,'click',function(event){
    location.href = "http://www.wrox.com";
});
EventUtil.addHandler(item2,'click',function(event){
    document.title = "I change the docunment's title";
});
EventUtil.addHandler(item3,'click',function(event){
    alert('hi!');
});
// 使用事件委託
var list = document.getElementById("myLinks");

EventUtil.addHandler(list,'click',function(event){
    event = EventUtil.getEvent(event);
    var target = EventUtil.getTarget(event);

    switch(target.id){
        case "goSomewhere":
            location.href = "http://www.wrox.com";
            break;
				
        case "doSomething":
            document.title = "I change the docunment's title";
            break;

        case "sayHi":
            alert('hi!');
            break;
    }
});
// 不使用 EventUtil 
var list = document.getElementById("myLinks");

list.addEventListener('click',function(event){
    switch(event.target.id){
        case "goSomewhere":
            location.href = "http://www.wrox.com";
            break;
				
        case "doSomething":
            document.title = "I change the docunment's title";
            break;

        case "sayHi":
            alert('hi!');
            break;
    }
});
  • 移除事件處理程序: 在不需要的時候移除事件處理程序,也是優化內存和性能的一個方案。內存中那些過時不用的“空事件處理程序”(dangling event handler)也是造成Web應用程序內存性能問題的主要原因。導致空事件處理程序兩種情況:

第一種:從文檔中移除帶有事件處理程序的元素

<div id = "myDiv">
    <input type = "button" value = "Click me" id = "myBtn">
</div>

<script type = "text/javascript">
    var btn = document.getElementById("myBtn");
    btn.onclick = function(){
        //do something
        btn.onclick = null;  //移除事假能處理程序
        document.getElementById("myDiv").innerHTML = "processing...";
    }
</script>

在事件處理程序中刪除按鈕也能阻止事件冒泡,目標元素在文檔中是事件冒泡的前提。

第二種:頁面卸載的時候

頁面被卸載之前沒有清理乾淨事件處理程序,那他們就會滯留在內存中。一般來說,最好的做法是在頁面卸載之前,先通過 onunload 事件處理程序移除所有事件處理程序。

END

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章