javascript
JavaScript 教程(二)
面向?qū)ο缶幊?/h1> 實(shí)例對(duì)象與 new 命令
JavaScript 語言具有很強(qiáng)的面向?qū)ο缶幊棠芰?#xff0c;這里介紹 JavaScript 面向?qū)ο缶幊痰幕A(chǔ)知識(shí)
對(duì)象是什么
面向?qū)ο缶幊?#xff08;Object Oriented Programming,縮寫為 OOP)是目前主流的編程范式。它將真實(shí)世界各種復(fù)雜的關(guān)系,抽象為一個(gè)個(gè)對(duì)象,然后由對(duì)象之間的分工與合作,完成對(duì)真實(shí)世界的模擬。每一個(gè)對(duì)象都是功能中心,具有明確分工,可以完成接受信息、處理數(shù)據(jù)、發(fā)出信息等任務(wù)。對(duì)象可以復(fù)用,通過繼承機(jī)制還可以定制。因此,面向?qū)ο缶幊叹哂徐`活、代碼可復(fù)用、高度模塊化等特點(diǎn),容易維護(hù)和開發(fā),比起由一系列函數(shù)或指令組成的傳統(tǒng)的過程式編程(procedural programming),更適合多人合作的大型軟件項(xiàng)目。那么,“對(duì)象”(object)到底是什么?我們從兩個(gè)層次來理解
對(duì)象是單個(gè)實(shí)物的抽象
一本書、一輛汽車、一個(gè)人都可以是對(duì)象,一個(gè)數(shù)據(jù)庫、一張網(wǎng)頁、一個(gè)與遠(yuǎn)程服務(wù)器的連接也可以是對(duì)象。當(dāng)實(shí)物被抽象成對(duì)象,實(shí)物之間的關(guān)系就變成了對(duì)象之間的關(guān)系,從而就可以模擬現(xiàn)實(shí)情況,針對(duì)對(duì)象進(jìn)行編程
對(duì)象是一個(gè)容器,封裝了屬性(property)和方法(method)
屬性是對(duì)象的狀態(tài),方法是對(duì)象的行為(完成某種任務(wù))。比如,我們可以把動(dòng)物抽象為animal對(duì)象,使用“屬性”記錄具體是那一種動(dòng)物,使用“方法”表示動(dòng)物的某種行為(奔跑、捕獵、休息等等)
構(gòu)造函數(shù)
面向?qū)ο缶幊痰牡谝徊?#xff0c;就是要生成對(duì)象。前面說過,對(duì)象是單個(gè)實(shí)物的抽象。通常需要一個(gè)模板,表示某一類實(shí)物的共同特征,然后對(duì)象根據(jù)這個(gè)模板生成。典型的面向?qū)ο缶幊陶Z言(比如 C++ 和 Java),都有“類”(class)這個(gè)概念。所謂“類”就是對(duì)象的模板,對(duì)象就是“類”的實(shí)例。但是,JavaScript 語言的對(duì)象體系,不是基于“類”的,而是基于構(gòu)造函數(shù)(constructor)和原型鏈(prototype)。JavaScript 語言使用構(gòu)造函數(shù)(constructor)作為對(duì)象的模板。所謂”構(gòu)造函數(shù)”,就是專門用來生成實(shí)例對(duì)象的函數(shù)。它就是對(duì)象的模板,描述實(shí)例對(duì)象的基本結(jié)構(gòu)。一個(gè)構(gòu)造函數(shù),可以生成多個(gè)實(shí)例對(duì)象,這些實(shí)例對(duì)象都有相同的結(jié)構(gòu)。構(gòu)造函數(shù)就是一個(gè)普通的函數(shù),但是有自己的特征和用法
var Vehicle = function () {this.price = 1000; };上面代碼中,Vehicle就是構(gòu)造函數(shù)。為了與普通函數(shù)區(qū)別,構(gòu)造函數(shù)名字的第一個(gè)字母通常大寫
構(gòu)造函數(shù)的特點(diǎn)有兩個(gè)。
1.函數(shù)體內(nèi)部使用了this關(guān)鍵字,代表了所要生成的對(duì)象實(shí)例
2.生成對(duì)象的時(shí)候,必須使用new命令
new 命令
基本用法
new命令的作用,就是執(zhí)行構(gòu)造函數(shù),返回一個(gè)實(shí)例對(duì)象
var Vehicle = function () {this.price = 1000; }; var v = new Vehicle(); v.price // 1000上面代碼通過new命令,讓構(gòu)造函數(shù)Vehicle生成一個(gè)實(shí)例對(duì)象,保存在變量v中。這個(gè)新生成的實(shí)例對(duì)象,從構(gòu)造函數(shù)Vehicle得到了price屬性。new命令執(zhí)行時(shí),構(gòu)造函數(shù)內(nèi)部的this,就代表了新生成的實(shí)例對(duì)象,this.price表示實(shí)例對(duì)象有一個(gè)price屬性,值是1000。使用new命令時(shí),根據(jù)需要構(gòu)造函數(shù)也可以接受參數(shù)
var Vehicle = function (p) {this.price = p; }; var v = new Vehicle(500);new命令本身就可以執(zhí)行構(gòu)造函數(shù),所以后面的構(gòu)造函數(shù)可以帶括號(hào),也可以不帶括號(hào)。下面兩行代碼是等價(jià)的,但是為了表示這里是函數(shù)調(diào)用,推薦使用括號(hào)
var v = new Vehicle(); // 推薦的寫法 var v = new Vehicle; // 不推薦的寫法一個(gè)很自然的問題是,如果忘了使用new命令,直接調(diào)用構(gòu)造函數(shù)會(huì)發(fā)生什么事?這種情況下,構(gòu)造函數(shù)就變成了普通函數(shù),并不會(huì)生成實(shí)例對(duì)象。而且由于后面會(huì)說到的原因,this這時(shí)代表全局對(duì)象,將造成一些意想不到的結(jié)果
var Vehicle = function (){this.price = 1000; }; var v = Vehicle(); v // undefined price // 1000上面代碼中,調(diào)用Vehicle構(gòu)造函數(shù)時(shí),忘了加上new命令。結(jié)果,變量v變成了undefined,而price屬性變成了全局變量。因此,應(yīng)該非常小心,避免不使用new命令、直接調(diào)用構(gòu)造函數(shù)。為了保證構(gòu)造函數(shù)必須與new命令一起使用,一個(gè)解決辦法是,構(gòu)造函數(shù)內(nèi)部使用嚴(yán)格模式,即第一行加上use strict。這樣的話,一旦忘了使用new命令,直接調(diào)用構(gòu)造函數(shù)就會(huì)報(bào)錯(cuò)
function Fubar(foo, bar){'use strict';this._foo = foo;this._bar = bar; } Fubar() // TypeError: Cannot set property '_foo' of undefined上面代碼的Fubar為構(gòu)造函數(shù),use strict命令保證了該函數(shù)在嚴(yán)格模式下運(yùn)行。由于嚴(yán)格模式中,函數(shù)內(nèi)部的this不能指向全局對(duì)象,默認(rèn)等于undefined,導(dǎo)致不加new調(diào)用會(huì)報(bào)錯(cuò)(JavaScript 不允許對(duì)undefined添加屬性)。另一個(gè)解決辦法,構(gòu)造函數(shù)內(nèi)部判斷是否使用new命令,如果發(fā)現(xiàn)沒有使用,則直接返回一個(gè)實(shí)例對(duì)象
function Fubar(foo, bar) {if (!(this instanceof Fubar)) {return new Fubar(foo, bar);}this._foo = foo;this._bar = bar; } Fubar(1, 2)._foo // 1 (new Fubar(1, 2))._foo // 1上面代碼中的構(gòu)造函數(shù),不管加不加new命令,都會(huì)得到同樣的結(jié)果
new 命令的原理
使用new命令時(shí),它后面的函數(shù)依次執(zhí)行下面的步驟
1.創(chuàng)建一個(gè)空對(duì)象,作為將要返回的對(duì)象實(shí)例
2.將這個(gè)空對(duì)象的原型,指向構(gòu)造函數(shù)的prototype屬性
3.將這個(gè)空對(duì)象賦值給函數(shù)內(nèi)部的this關(guān)鍵字
4.開始執(zhí)行構(gòu)造函數(shù)內(nèi)部的代碼
也就是說,構(gòu)造函數(shù)內(nèi)部,this指的是一個(gè)新生成的空對(duì)象,所有針對(duì)this的操作,都會(huì)發(fā)生在這個(gè)空對(duì)象上。構(gòu)造函數(shù)之所以叫“構(gòu)造函數(shù)”,就是說這個(gè)函數(shù)的目的,就是操作一個(gè)空對(duì)象(即this對(duì)象),將其“構(gòu)造”為需要的樣子。如果構(gòu)造函數(shù)內(nèi)部有return語句,而且return后面跟著一個(gè)對(duì)象,new命令會(huì)返回return語句指定的對(duì)象;否則,就會(huì)不管return語句,返回this對(duì)象
var Vehicle = function () {this.price = 1000;return 1000; }; (new Vehicle()) === 1000 // false上面代碼中,構(gòu)造函數(shù)Vehicle的return語句返回一個(gè)數(shù)值。這時(shí),new命令就會(huì)忽略這個(gè)return語句,返回“構(gòu)造”后的this對(duì)象。但是,如果return語句返回的是一個(gè)跟this無關(guān)的新對(duì)象,new命令會(huì)返回這個(gè)新對(duì)象,而不是this對(duì)象。這一點(diǎn)需要特別引起注意
var Vehicle = function (){this.price = 1000;return { price: 2000 }; }; (new Vehicle()).price // 2000上面代碼中,構(gòu)造函數(shù)Vehicle的return語句,返回的是一個(gè)新對(duì)象。new命令會(huì)返回這個(gè)對(duì)象,而不是this對(duì)象。另一方面,如果對(duì)普通函數(shù)(內(nèi)部沒有this關(guān)鍵字的函數(shù))使用new命令,則會(huì)返回一個(gè)空對(duì)象
function getMessage() {return 'this is a message'; } var msg = new getMessage(); msg // {} typeof msg // "object"上面代碼中,getMessage是一個(gè)普通函數(shù),返回一個(gè)字符串。對(duì)它使用new命令,會(huì)得到一個(gè)空對(duì)象。這是因?yàn)閚ew命令總是返回一個(gè)對(duì)象,要么是實(shí)例對(duì)象,要么是return語句指定的對(duì)象。本例中,return語句返回的是字符串,所以new命令就忽略了該語句。new命令簡(jiǎn)化的內(nèi)部流程,可以用下面的代碼表示
function _new(/* 構(gòu)造函數(shù) */ constructor, /* 構(gòu)造函數(shù)參數(shù) */ params) { var args = [].slice.call(arguments); // 將 arguments 對(duì)象轉(zhuǎn)為數(shù)組 var constructor = args.shift(); // 取出構(gòu)造函數(shù) var context = Object.create(constructor.prototype); // 創(chuàng)建一個(gè)空對(duì)象,繼承構(gòu)造函數(shù)的 prototype 屬性 var result = constructor.apply(context, args); // 執(zhí)行構(gòu)造函數(shù) return (typeof result === 'object' && result != null) ? result : context; // 如果返回結(jié)果是對(duì)象,就直接返回,否則返回 context 對(duì)象 } var actor = _new(Person, '張三', 28); // 實(shí)例new.target
函數(shù)內(nèi)部可以使用new.target屬性。如果當(dāng)前函數(shù)是new命令調(diào)用,new.target指向當(dāng)前函數(shù),否則為undefined
function f() {console.log(new.target === f); } f() // false new f() // true使用這個(gè)屬性,可以判斷函數(shù)調(diào)用的時(shí)候,是否使用new命令
function f() {if (!new.target) {throw new Error('請(qǐng)使用 new 命令調(diào)用!');}// ... } f() // Uncaught Error: 請(qǐng)使用 new 命令調(diào)用!Object.create() 創(chuàng)建實(shí)例對(duì)象
構(gòu)造函數(shù)作為模板,可以生成實(shí)例對(duì)象。但是,有時(shí)拿不到構(gòu)造函數(shù),只能拿到一個(gè)現(xiàn)有的對(duì)象。我們希望以這個(gè)現(xiàn)有的對(duì)象作為模板,生成新的實(shí)例對(duì)象,這時(shí)就可以使用Object.create()方法
var person1 = {name: '張三',age: 38,greeting: function() {console.log('Hi! I\'m ' + this.name + '.');} }; var person2 = Object.create(person1); person2.name // 張三 person2.greeting() // Hi! I'm 張三.上面代碼中,對(duì)象person1是person2的模板,后者繼承了前者的屬性和方法
this 關(guān)鍵字
涵義
this關(guān)鍵字是一個(gè)非常重要的語法點(diǎn)。毫不夸張地說,不理解它的含義大部分開發(fā)任務(wù)都無法完成。this可以用在構(gòu)造函數(shù)之中,表示實(shí)例對(duì)象;除此之外,this還可以用在別的場(chǎng)合。但不管是什么場(chǎng)合,this都有一個(gè)共同點(diǎn):它總是返回一個(gè)對(duì)象;簡(jiǎn)單說,this就是屬性或方法“當(dāng)前”所在的對(duì)象
var person = {name: '張三',describe: function () { return '姓名:'+ this.name; } }; person.describe() // "姓名:張三"上面代碼中,this.name表示name屬性所在的那個(gè)對(duì)象。由于this.name是在describe方法中調(diào)用,而describe方法所在的當(dāng)前對(duì)象是person,因此this指向person,this.name就是person.name。由于對(duì)象的屬性可以賦給另一個(gè)對(duì)象,所以屬性所在的當(dāng)前對(duì)象是可變的,即this的指向是可變的
var A = {name: '張三',describe: function () { return '姓名:'+ this.name; } }; var B = { name: '李四' }; B.describe = A.describe; B.describe() // "姓名:李四"上面代碼中,A.describe屬性被賦給B,于是B.describe就表示describe方法所在的當(dāng)前對(duì)象是B,所以this.name就指向B.name。稍稍重構(gòu)這個(gè)例子,this的動(dòng)態(tài)指向就能看得更清楚
function f() { return '姓名:'+ this.name; } var A = { name: '張三', describe: f }; var B = { name: '李四', describe: f }; A.describe() // "姓名:張三" B.describe() // "姓名:李四"只要函數(shù)被賦給另一個(gè)變量,this的指向就會(huì)變
var A = {name: '張三',describe: function () { return '姓名:'+ this.name; } }; var name = '李四'; var f = A.describe; f() // "姓名:李四"上面代碼中,A.describe被賦值給變量f,內(nèi)部的this就會(huì)指向f運(yùn)行時(shí)所在的對(duì)象(本例是頂層對(duì)象)。再看一個(gè)網(wǎng)頁編程的例子
<input type="text" name="age" size=3 onChange="validate(this, 18, 99);"> <script> function validate(obj, lowval, hival){if ((obj.value < lowval) || (obj.value > hival))console.log('Invalid Value!'); } </script>上面代碼是一個(gè)文本輸入框,每當(dāng)用戶輸入一個(gè)值,就會(huì)調(diào)用onChange回調(diào)函數(shù),驗(yàn)證這個(gè)值是否在指定范圍。瀏覽器會(huì)向回調(diào)函數(shù)傳入當(dāng)前對(duì)象,因此this就代表傳入當(dāng)前對(duì)象(即文本框),然后就可以從this.value上面讀到用戶的輸入值
總結(jié)一下,JavaScript 語言之中,一切皆對(duì)象,運(yùn)行環(huán)境也是對(duì)象,所以函數(shù)都是在某個(gè)對(duì)象之中運(yùn)行,this就是函數(shù)運(yùn)行時(shí)所在的對(duì)象(環(huán)境)。這本來并不會(huì)讓用戶糊涂,但是 JavaScript 支持運(yùn)行環(huán)境動(dòng)態(tài)切換,也就是說,this的指向是動(dòng)態(tài)的,沒有辦法事先確定到底指向哪個(gè)對(duì)象,這才是最讓初學(xué)者感到困惑的地方
實(shí)質(zhì)
JavaScript 語言之所以有 this 的設(shè)計(jì),跟內(nèi)存里面的數(shù)據(jù)結(jié)構(gòu)有關(guān)系
var obj = { foo: 5 };上面的代碼將一個(gè)對(duì)象賦值給變量obj。JavaScript 引擎會(huì)先在內(nèi)存里面,生成一個(gè)對(duì)象{ foo: 5 },然后把這個(gè)對(duì)象的內(nèi)存地址賦值給變量obj。也就是說,變量obj是一個(gè)地址(reference)。后面如果要讀取obj.foo,引擎先從obj拿到內(nèi)存地址,然后再從該地址讀出原始的對(duì)象,返回它的foo屬性。原始的對(duì)象以字典結(jié)構(gòu)保存,每一個(gè)屬性名都對(duì)應(yīng)一個(gè)屬性描述對(duì)象。舉例來說,上面例子的foo屬性,實(shí)際上是以下面的形式保存的
{foo: {[[value]]: 5[[writable]]: true[[enumerable]]: true[[configurable]]: true} }注意,foo屬性的值保存在屬性描述對(duì)象的value屬性里面。這樣的結(jié)構(gòu)是很清晰的,問題在于屬性的值可能是一個(gè)函數(shù);這時(shí),引擎會(huì)將函數(shù)單獨(dú)保存在內(nèi)存中,然后再將函數(shù)的地址賦值給foo屬性的value屬性
var obj = { foo: function () {} };由于函數(shù)是一個(gè)單獨(dú)的值,所以它可以在不同的環(huán)境(上下文)執(zhí)行
var f = function () {}; var obj = { f: f }; f() // 單獨(dú)執(zhí)行 obj.f() // obj 環(huán)境執(zhí)行JavaScript 允許在函數(shù)體內(nèi)部,引用當(dāng)前環(huán)境的其他變量
var f = function () {console.log(x); };上面代碼中,函數(shù)體里面使用了變量x,該變量由運(yùn)行環(huán)境提供。現(xiàn)在問題就來了,由于函數(shù)可以在不同的運(yùn)行環(huán)境執(zhí)行,所以需要有一種機(jī)制,能夠在函數(shù)體內(nèi)部獲得當(dāng)前的運(yùn)行環(huán)境(context)。所以,this就出現(xiàn)了,它的設(shè)計(jì)目的就是在函數(shù)體內(nèi)部,指代函數(shù)當(dāng)前的運(yùn)行環(huán)境
var f = function () { console.log(this.x);} var x = 1; var obj = { f: f, x: 2}; // 單獨(dú)執(zhí)行 f() // 1 // obj 環(huán)境執(zhí)行 obj.f() // 2上面代碼中,函數(shù)f在全局環(huán)境執(zhí)行,this.x指向全局環(huán)境的x;在obj環(huán)境執(zhí)行,this.x指向obj.x
使用場(chǎng)合
this主要有以下幾個(gè)使用場(chǎng)合
全局環(huán)境
全局環(huán)境使用this,它指的就是頂層對(duì)象window
this === window // true function f() {console.log(this === window); } f() // true上面代碼說明,不管是不是在函數(shù)內(nèi)部,只要是在全局環(huán)境下運(yùn)行,this就是指頂層對(duì)象window
構(gòu)造函數(shù)
構(gòu)造函數(shù)中的this,指的是實(shí)例對(duì)象
var Obj = function (p) {this.p = p; }; var o = new Obj('Hello World!'); o.p // "Hello World!"上面代碼定義了一個(gè)構(gòu)造函數(shù)Obj。由于this指向?qū)嵗龑?duì)象,所以在構(gòu)造函數(shù)內(nèi)部定義this.p,就相當(dāng)于定義實(shí)例對(duì)象有一個(gè)p屬性
對(duì)象的方法
如果對(duì)象的方法里面包含this,this的指向就是方法運(yùn)行時(shí)所在的對(duì)象。該方法賦值給另一個(gè)對(duì)象,就會(huì)改變this的指向。但是,這條規(guī)則很不容易把握。請(qǐng)看下面的代碼
var obj ={foo: function () { console.log(this); } }; obj.foo() // obj上面代碼中,obj.foo方法執(zhí)行時(shí),它內(nèi)部的this指向obj。但是,下面這幾種用法,都會(huì)改變this的指向
// 情況一 (obj.foo = obj.foo)() // window // 情況二 (false || obj.foo)() // window // 情況三 (1, obj.foo)() // window上面代碼中,obj.foo就是一個(gè)值。這個(gè)值真正調(diào)用的時(shí)候,運(yùn)行環(huán)境已經(jīng)不是obj了,而是全局環(huán)境,所以this不再指向obj。可以這樣理解,JavaScript 引擎內(nèi)部,obj和obj.foo儲(chǔ)存在兩個(gè)內(nèi)存地址,稱為地址一和地址二。obj.foo()這樣調(diào)用時(shí),是從地址一調(diào)用地址二,因此地址二的運(yùn)行環(huán)境是地址一,this指向obj。但是,上面三種情況,都是直接取出地址二進(jìn)行調(diào)用,這樣的話,運(yùn)行環(huán)境就是全局環(huán)境,因此this指向全局環(huán)境。上面三種情況等同于下面的代碼
// 情況一 (obj.foo = function () { console.log(this);})() // 等同于 (function () { console.log(this);})() // 情況二 (false || function () { console.log(this);})() // 情況三 (1, function () { console.log(this);})()如果this所在的方法不在對(duì)象的第一層,這時(shí)this只是指向當(dāng)前一層的對(duì)象,而不會(huì)繼承更上面的層
var a = {p: 'Hello',b: {m: function() { console.log(this.p); }} }; a.b.m() // undefined上面代碼中,a.b.m方法在a對(duì)象的第二層,該方法內(nèi)部的this不是指向a,而是指向a.b,因?yàn)閷?shí)際執(zhí)行的是下面的代碼
var b = {m: function() { console.log(this.p); } }; var a = { p: 'Hello', b: b }; (a.b).m() // 等同于 b.m()如果要達(dá)到預(yù)期效果,只有寫成下面這樣
var a = {b: {m: function() { console.log(this.p); },p: 'Hello'} };如果這時(shí)將嵌套對(duì)象內(nèi)部的方法賦值給一個(gè)變量,this依然會(huì)指向全局對(duì)象
var a = {b: {m: function() { console.log(this.p); },p: 'Hello'} }; var hello = a.b.m; hello() // undefined上面代碼中,m是多層對(duì)象內(nèi)部的一個(gè)方法。為求簡(jiǎn)便,將其賦值給hello變量,結(jié)果調(diào)用時(shí),this指向了頂層對(duì)象。為了避免這個(gè)問題,可以只將m所在的對(duì)象賦值給hello,這樣調(diào)用時(shí),this的指向就不會(huì)變
var hello = a.b; hello.m() // Hello使用注意點(diǎn)
避免多層 this
由于this的指向是不確定的,所以切勿在函數(shù)中包含多層的this
var o = {f1: function () {console.log(this);var f2 = function () { console.log(this); }();} } o.f1() // Object // Window上面代碼包含兩層this,結(jié)果運(yùn)行后,第一層指向?qū)ο髈,第二層指向全局對(duì)象,因?yàn)閷?shí)際執(zhí)行的是下面的代碼
var temp = function () { console.log(this); }; var o = {f1: function () { console.log(this); var f2 = temp(); } }一個(gè)解決方法是在第二層改用一個(gè)指向外層this的變量
var o = {f1: function() {console.log(this);var that = this;var f2 = function() { console.log(that); }();} } o.f1() // Object // Object上面代碼定義了變量that,固定指向外層的this,然后在內(nèi)層使用that,就不會(huì)發(fā)生this指向的改變。事實(shí)上,使用一個(gè)變量固定this的值,然后內(nèi)層函數(shù)調(diào)用這個(gè)變量,是非常常見的做法。JavaScript 提供了嚴(yán)格模式,也可以硬性避免這種問題。嚴(yán)格模式下,如果函數(shù)內(nèi)部的this指向頂層對(duì)象,就會(huì)報(bào)錯(cuò)
var counter = { count: 0 }; counter.inc = function () {'use strict';this.count++ }; var f = counter.inc; f() // TypeError: Cannot read property 'count' of undefined上面代碼中,inc方法通過'use strict'聲明采用嚴(yán)格模式,這時(shí)內(nèi)部的this一旦指向頂層對(duì)象,就會(huì)報(bào)錯(cuò)
避免數(shù)組處理方法中的 this
數(shù)組的map和foreach方法,允許提供一個(gè)函數(shù)作為參數(shù)。這個(gè)函數(shù)內(nèi)部不應(yīng)該使用this
var o = {v: 'hello',p: [ 'a1', 'a2' ],f: function f() {this.p.forEach(function (item) { console.log(this.v + ' ' + item); });} } o.f() // undefined a1 // undefined a2上面代碼中,foreach方法的回調(diào)函數(shù)中的this,其實(shí)是指向window對(duì)象,因此取不到o.v的值。原因跟上一段的多層this是一樣的,就是內(nèi)層的this不指向外部,而指向頂層對(duì)象。解決這個(gè)問題的一種方法,就是前面提到的,使用中間變量固定this
var o = {v: 'hello',p: [ 'a1', 'a2' ],f: function f() {var that = this;this.p.forEach(function (item) { console.log(that.v+' '+item); });} } o.f() // hello a1 // hello a2另一種方法是將this當(dāng)作foreach方法的第二個(gè)參數(shù),固定它的運(yùn)行環(huán)境
var o = {v: 'hello',p: [ 'a1', 'a2' ],f: function f() {this.p.forEach(function (item) { console.log(this.v + ' ' + item); }, this);} } o.f() // hello a1 // hello a2避免回調(diào)函數(shù)中的 this
回調(diào)函數(shù)中的this往往會(huì)改變指向,最好避免使用
var o = new Object(); o.f = function () { console.log(this === o);} $('#button').on('click', o.f); // jQuery 的寫法上面代碼中,點(diǎn)擊按鈕以后,控制臺(tái)會(huì)顯示false。原因是此時(shí)this不再指向o對(duì)象,而是指向按鈕的 DOM 對(duì)象,因?yàn)閒方法是在按鈕對(duì)象的環(huán)境中被調(diào)用的。這種細(xì)微的差別,很容易在編程中忽視,導(dǎo)致難以察覺的錯(cuò)誤。為了解決這個(gè)問題,可以采用下面的一些方法對(duì)this進(jìn)行綁定,也就是使得this固定指向某個(gè)對(duì)象,減少不確定性
綁定 this 的方法
this的動(dòng)態(tài)切換,固然為 JavaScript 創(chuàng)造了巨大的靈活性,但也使得編程變得困難和模糊。有時(shí),需要把this固定下來,避免出現(xiàn)意想不到的情況。JavaScript 提供了call、apply、bind這三個(gè)方法,來切換/固定this的指向
Function.prototype.call()
函數(shù)實(shí)例的call方法,可以指定函數(shù)內(nèi)部this的指向(即函數(shù)執(zhí)行時(shí)所在的作用域),然后在所指定的作用域中,調(diào)用該函數(shù)
var obj = {}; var f = function () { return this; }; f() === window // true f.call(obj) === obj // true上面代碼中,全局環(huán)境運(yùn)行函數(shù)f時(shí),this指向全局環(huán)境(瀏覽器為window對(duì)象);call方法可以改變this的指向,指定this指向?qū)ο髈bj,然后在對(duì)象obj的作用域中運(yùn)行函數(shù)f。call方法的參數(shù),應(yīng)該是一個(gè)對(duì)象。如果參數(shù)為空、null和undefined,則默認(rèn)傳入全局對(duì)象
var n = 123; var obj = { n: 456 }; function a() { console.log(this.n);} a.call() // 123 a.call(null) // 123 a.call(undefined) // 123 a.call(window) // 123 a.call(obj) // 456上面代碼中,a函數(shù)中的this關(guān)鍵字,如果指向全局對(duì)象,返回結(jié)果為123。如果使用call方法將this關(guān)鍵字指向obj對(duì)象,返回結(jié)果為456。可以看到,如果call方法沒有參數(shù),或者參數(shù)為null或undefined,則等同于指向全局對(duì)象。如果call方法的參數(shù)是一個(gè)原始值,那么這個(gè)原始值會(huì)自動(dòng)轉(zhuǎn)成對(duì)應(yīng)的包裝對(duì)象,然后傳入call方法
var f = function () {return this; }; f.call(5) // Number {[[PrimitiveValue]]: 5}上面代碼中,call的參數(shù)為5,不是對(duì)象,會(huì)被自動(dòng)轉(zhuǎn)成包裝對(duì)象(Number的實(shí)例),綁定f內(nèi)部的this。call方法還可以接受多個(gè)參數(shù)
func.call(thisValue, arg1, arg2, ...)call的第一個(gè)參數(shù)就是this所要指向的那個(gè)對(duì)象,后面的參數(shù)則是函數(shù)調(diào)用時(shí)所需的參數(shù)
function add(a, b) { return a + b;} add.call(this, 1, 2) // 3上面代碼中,call方法指定函數(shù)add內(nèi)部的this綁定當(dāng)前環(huán)境(對(duì)象),并且參數(shù)為1和2,因此函數(shù)add運(yùn)行后得到3。call方法的一個(gè)應(yīng)用是調(diào)用對(duì)象的原生方法
var obj = {}; // object的hasOwnProperty()方法返回一個(gè)布爾值,判斷對(duì)象是否包含特定的自身(非繼承)屬性 obj.hasOwnProperty('toString') // false // 覆蓋掉繼承的 hasOwnProperty 方法 obj.hasOwnProperty = function () { return true;}; obj.hasOwnProperty('toString') // true Object.prototype.hasOwnProperty.call(obj, 'toString') // false上面代碼中,hasOwnProperty是obj對(duì)象繼承的方法,如果這個(gè)方法一旦被覆蓋,就不會(huì)得到正確結(jié)果。call方法可以解決這個(gè)問題,它將hasOwnProperty方法的原始定義放到obj對(duì)象上執(zhí)行,這樣無論obj上有沒有同名方法,都不會(huì)影響結(jié)果
Function.prototype.apply()
apply方法的作用與call方法類似,也是改變this指向,然后再調(diào)用該函數(shù)。唯一的區(qū)別就是,它接收一個(gè)數(shù)組作為函數(shù)執(zhí)行時(shí)的參數(shù),使用格式如下:
func.apply(thisValue, [arg1, arg2, ...])apply方法的第一個(gè)參數(shù)也是this所要指向的那個(gè)對(duì)象,如果設(shè)為null或undefined,則等同于指定全局對(duì)象。第二個(gè)參數(shù)則是一個(gè)數(shù)組,該數(shù)組的所有成員依次作為參數(shù),傳入原函數(shù)。原函數(shù)的參數(shù),在call方法中必須一個(gè)個(gè)添加,但是在apply方法中,必須以數(shù)組形式添加
function f(x, y){ console.log(x + y);} f.call(null, 1, 1) // 2 f.apply(null, [1, 1]) // 2上面代碼中,f函數(shù)本來接受兩個(gè)參數(shù),使用apply方法以后,就變成可以接受一個(gè)數(shù)組作為參數(shù)。利用這一點(diǎn),可以做一些有趣的應(yīng)用
找出數(shù)組最大元素
JavaScript 不提供找出數(shù)組最大元素的函數(shù)。結(jié)合使用apply方法和Math.max方法,就可以返回?cái)?shù)組的最大元素
var a = [10, 2, 4, 15, 9]; Math.max.apply(null, a) // 15將數(shù)組的空元素變?yōu)閡ndefined
通過apply方法,利用Array構(gòu)造函數(shù)將數(shù)組的空元素變成undefined
Array.apply(null, ['a', ,'b']) // [ 'a', undefined, 'b' ]空元素與undefined的差別在于,數(shù)組的forEach方法會(huì)跳過空元素,但是不會(huì)跳過undefined。因此,遍歷內(nèi)部元素的時(shí)候,會(huì)得到不同的結(jié)果
var a = ['a', , 'b']; function print(i) { console.log(i);} a.forEach(print) // a // b Array.apply(null, a).forEach(print) // a // undefined // b轉(zhuǎn)換類似數(shù)組的對(duì)象
另外,利用數(shù)組對(duì)象的slice方法,可以將一個(gè)類似數(shù)組的對(duì)象(比如arguments對(duì)象)轉(zhuǎn)為真正的數(shù)組
Array.prototype.slice.apply({0: 1, length: 1}) // [1] Array.prototype.slice.apply({0: 1}) // [] Array.prototype.slice.apply({0: 1, length: 2}) // [1, undefined] Array.prototype.slice.apply({length: 1}) // [undefined]上面代碼的apply方法的參數(shù)都是對(duì)象,但是返回結(jié)果都是數(shù)組,這就起到了將對(duì)象轉(zhuǎn)成數(shù)組的目的。從上面代碼可以看到,這個(gè)方法起作用的前提是,被處理的對(duì)象必須有l(wèi)ength屬性,以及相對(duì)應(yīng)的數(shù)字鍵
綁定回調(diào)函數(shù)的對(duì)象
前面的按鈕點(diǎn)擊事件的例子,可以改寫如下:
var o = new Object(); o.f = function () { console.log(this === o);} var f = function (){o.f.apply(o); // 或者 o.f.call(o); }; $('#button').on('click', f); // jQuery 的寫法上面代碼中,點(diǎn)擊按鈕以后,控制臺(tái)將會(huì)顯示true。由于apply方法(或者call方法)不僅綁定函數(shù)執(zhí)行時(shí)所在的對(duì)象,還會(huì)立即執(zhí)行函數(shù),因此不得不把綁定語句寫在一個(gè)函數(shù)體內(nèi)。更簡(jiǎn)潔的寫法是采用下面介紹的bind方法
Function.prototype.bind()
bind方法用于將函數(shù)體內(nèi)的this綁定到某個(gè)對(duì)象,然后返回一個(gè)新函數(shù)
var d = new Date(); d.getTime() // 1553063951536 var print = d.getTime; print() // Uncaught TypeError: this is not a Date object.上面代碼中,我們將d.getTime方法賦給變量print,然后調(diào)用print就報(bào)錯(cuò)了。這是因?yàn)間etTime方法內(nèi)部的this,綁定Date對(duì)象的實(shí)例,賦給變量print以后,內(nèi)部的this已經(jīng)不指向Date對(duì)象的實(shí)例了。bind方法可以解決這個(gè)問題
var print = d.getTime.bind(d); print() // 1553063951536上面代碼中,bind方法將getTime方法內(nèi)部的this綁定到d對(duì)象,這時(shí)就可以安全地將這個(gè)方法賦值給其他變量了。bind方法的參數(shù)就是所要綁定this的對(duì)象,下面是一個(gè)更清晰的例子
var counter = {count: 0,inc: function () { this.count++; } }; var func = counter.inc.bind(counter); func(); counter.count // 1上面代碼中,counter.inc方法被賦值給變量func。這時(shí)必須用bind方法將inc內(nèi)部的this,綁定到counter,否則就會(huì)出錯(cuò)。this綁定到其他對(duì)象也是可以的
var counter = {count: 0,inc: function () { this.count++; } }; var obj = { count: 100 }; var func = counter.inc.bind(obj); func(); obj.count // 101上面代碼中,bind方法將inc方法內(nèi)部的this,綁定到obj對(duì)象。結(jié)果調(diào)用func函數(shù)以后,遞增的就是obj內(nèi)部的count屬性。bind還可以接受更多的參數(shù),將這些參數(shù)綁定原函數(shù)的參數(shù)
var add = function (x, y) { return x * this.m + y * this.n;} var obj = { m: 2, n: 3}; var newAdd = add.bind(obj, 5); newAdd(6) // 28上面代碼中,bind方法除了綁定this對(duì)象,還將add函數(shù)的第一個(gè)參數(shù)x綁定成5,然后返回一個(gè)新函數(shù)newAdd,這個(gè)函數(shù)只要再接受一個(gè)參數(shù)y就能運(yùn)行了。如果bind方法的第一個(gè)參數(shù)是null或undefined,等于將this綁定到全局對(duì)象,函數(shù)運(yùn)行時(shí)this指向頂層對(duì)象(瀏覽器為window)
function add(x, y) { return x + y;} var plus = add.bind(null, 5); plus(10) // 15上面代碼中,函數(shù)add內(nèi)部并沒有this,使用bind方法的主要目的是綁定參數(shù)x,以后每次運(yùn)行新函數(shù)plus,就只需要提供另一個(gè)參數(shù)y就夠了。而且因?yàn)閍dd內(nèi)部沒有this,所以bind的第一個(gè)參數(shù)是null,不過這里如果是其他對(duì)象,也沒有影響。bind方法有一些使用注意點(diǎn)
每一次返回一個(gè)新函數(shù)
bind方法每運(yùn)行一次,就返回一個(gè)新函數(shù),這會(huì)產(chǎn)生一些問題。比如,監(jiān)聽事件的時(shí)候,不能寫成下面這樣
element.addEventListener('click', o.m.bind(o));上面代碼中,click事件綁定bind方法生成的一個(gè)匿名函數(shù)。這樣會(huì)導(dǎo)致無法取消綁定,所以,下面的代碼是無效的
element.removeEventListener('click', o.m.bind(o));正確的方法是寫成下面這樣:
var listener = o.m.bind(o); element.addEventListener('click', listener); // ... element.removeEventListener('click', listener);結(jié)合回調(diào)函數(shù)使用
回調(diào)函數(shù)是 JavaScript 最常用的模式之一,但是一個(gè)常見的錯(cuò)誤是,將包含this的方法直接當(dāng)作回調(diào)函數(shù)。解決方法就是使用bind方法,將counter.inc綁定counter
var counter = {count: 0,inc: function () {'use strict'; this.count++; } }; function callIt(callback) { callback();} callIt(counter.inc.bind(counter)); counter.count // 1上面代碼中,callIt方法會(huì)調(diào)用回調(diào)函數(shù)。這時(shí)如果直接把counter.inc傳入,調(diào)用時(shí)counter.inc內(nèi)部的this就會(huì)指向全局對(duì)象。使用bind方法將counter.inc綁定counter以后,就不會(huì)有這個(gè)問題,this總是指向counter。還有一種情況比較隱蔽,就是某些數(shù)組方法可以接受一個(gè)函數(shù)當(dāng)作參數(shù)。這些函數(shù)內(nèi)部的this指向,很可能也會(huì)出錯(cuò)
var obj = {name: '張三',times: [1, 2, 3],print: function () {this.times.forEach(function (n) { console.log(this.name); });} }; obj.print() // 沒有任何輸出上面代碼中,obj.print內(nèi)部this.times的this是指向obj的,這個(gè)沒有問題。但是,forEach方法的回調(diào)函數(shù)內(nèi)部的this.name卻是指向全局對(duì)象,導(dǎo)致沒有辦法取到值。稍微改動(dòng)一下,就可以看得更清楚
obj.print = function () {this.times.forEach(function (n) { console.log(this === window); }); }; obj.print() // true // true // true解決這個(gè)問題,也是通過bind方法綁定this
obj.print = function () {this.times.forEach(function (n) { console.log(this.name); }.bind(this)); }; obj.print() // 張三 // 張三 // 張三結(jié)合call方法使用
利用bind方法,可以改寫一些 JavaScript 原生方法的使用形式,以數(shù)組的slice方法為例
[1, 2, 3].slice(0, 1) // [1] // 等同于 Array.prototype.slice.call([1, 2, 3], 0, 1) // [1]上面的代碼中,數(shù)組的slice方法從[1, 2, 3]里面,按照指定位置和長(zhǎng)度切分出另一個(gè)數(shù)組。這樣做的本質(zhì)是在[1, 2, 3]上面調(diào)用Array.prototype.slice方法,因此可以用call方法表達(dá)這個(gè)過程,得到同樣的結(jié)果。call方法實(shí)質(zhì)上是調(diào)用Function.prototype.call方法,因此上面的表達(dá)式可以用bind方法改寫
var slice = Function.prototype.call.bind(Array.prototype.slice); slice([1, 2, 3], 0, 1) // [1]上面代碼的含義就是,將Array.prototype.slice變成Function.prototype.call方法所在的對(duì)象,調(diào)用時(shí)就變成了Array.prototype.slice.call。類似的寫法還可以用于其他數(shù)組方法
var push = Function.prototype.call.bind(Array.prototype.push); var pop = Function.prototype.call.bind(Array.prototype.pop); var a = [1 ,2 ,3]; push(a, 4) a // [1, 2, 3, 4] pop(a) a // [1, 2, 3]如果再進(jìn)一步,將Function.prototype.call方法綁定到Function.prototype.bind對(duì)象,就意味著bind的調(diào)用形式也可以被改寫
function f() { console.log(this.v);} var o = { v: 123 }; var bind = Function.prototype.call.bind(Function.prototype.bind); bind(f, o)() // 123上面代碼的含義就是,將Function.prototype.bind方法綁定在Function.prototype.call上面,所以bind方法就可以直接使用,不需要在函數(shù)實(shí)例上使用
對(duì)象的繼承
面向?qū)ο缶幊毯苤匾囊粋€(gè)方面,就是對(duì)象的繼承。A 對(duì)象通過繼承 B 對(duì)象,就能直接擁有 B 對(duì)象的所有屬性和方法。這對(duì)于代碼的復(fù)用是非常有用的。大部分面向?qū)ο蟮木幊陶Z言,都是通過“類”(class)實(shí)現(xiàn)對(duì)象的繼承。傳統(tǒng)上,JavaScript 語言的繼承不通過 class,而是通過“原型對(duì)象”(prototype)實(shí)現(xiàn),這里主要介紹 JavaScript 的原型鏈繼承。ES6 引入了 class 語法,基于 class 的繼承暫不在這里介紹
原型對(duì)象概述
構(gòu)造函數(shù)的缺點(diǎn)
JavaScript 通過構(gòu)造函數(shù)生成新對(duì)象,因此構(gòu)造函數(shù)可以視為對(duì)象的模板。實(shí)例對(duì)象的屬性和方法,可以定義在構(gòu)造函數(shù)內(nèi)部
function Cat (name, color) { this.name = name; this.color = color;} var cat1 = new Cat('大毛', '白色'); cat1.name // '大毛' cat1.color // '白色'上面代碼中,Cat函數(shù)是一個(gè)構(gòu)造函數(shù),函數(shù)內(nèi)部定義了name屬性和color屬性,所有實(shí)例對(duì)象(上例是cat1)都會(huì)生成這兩個(gè)屬性,即這兩個(gè)屬性會(huì)定義在實(shí)例對(duì)象上面。通過構(gòu)造函數(shù)為實(shí)例對(duì)象定義屬性,雖然很方便,但是有一個(gè)缺點(diǎn)。同一個(gè)構(gòu)造函數(shù)的多個(gè)實(shí)例之間,無法共享屬性,從而造成對(duì)系統(tǒng)資源的浪費(fèi)
function Cat(name, color) {this.name = name;this.color = color;this.meow = function () { console.log('喵喵'); }; } var cat1 = new Cat('大毛', '白色'); var cat2 = new Cat('二毛', '黑色'); cat1.meow === cat2.meow // false上面代碼中,cat1和cat2是同一個(gè)構(gòu)造函數(shù)的兩個(gè)實(shí)例,它們都具有meow方法。由于meow方法是生成在每個(gè)實(shí)例對(duì)象上面,所以兩個(gè)實(shí)例就生成了兩次。也就是說,每新建一個(gè)實(shí)例,就會(huì)新建一個(gè)meow方法。這既沒有必要,又浪費(fèi)系統(tǒng)資源,因?yàn)樗衜eow方法都是同樣的行為,完全應(yīng)該共享。這個(gè)問題的解決方法,就是 JavaScript 的原型對(duì)象(prototype)
prototype 屬性的作用
JavaScript 繼承機(jī)制的設(shè)計(jì)思想就是,原型對(duì)象的所有屬性和方法,都能被實(shí)例對(duì)象共享。也就是說,如果屬性和方法定義在原型上,那么所有實(shí)例對(duì)象就能共享,不僅節(jié)省了內(nèi)存,還體現(xiàn)了實(shí)例對(duì)象之間的聯(lián)系。下面,先看怎么為對(duì)象指定原型。JavaScript 規(guī)定,每個(gè)函數(shù)都有一個(gè)prototype屬性,指向一個(gè)對(duì)象
function f() {} typeof f.prototype // "object"上面代碼中,函數(shù)f默認(rèn)具有prototype屬性,指向一個(gè)對(duì)象。對(duì)于普通函數(shù)來說,該屬性基本無用。但是,對(duì)于構(gòu)造函數(shù)來說,生成實(shí)例的時(shí)候,該屬性會(huì)自動(dòng)成為實(shí)例對(duì)象的原型
function f() {} typeof f.prototype // "object"上面代碼中,函數(shù)f默認(rèn)具有prototype屬性,指向一個(gè)對(duì)象。對(duì)于普通函數(shù)來說,該屬性基本無用。但是,對(duì)于構(gòu)造函數(shù)來說,生成實(shí)例的時(shí)候,該屬性會(huì)自動(dòng)成為實(shí)例對(duì)象的原型
function Animal(name) { this.name = name;} Animal.prototype.color = 'white'; var cat1 = new Animal('大毛'); var cat2 = new Animal('二毛'); cat1.color // 'white' cat2.color // 'white'上面代碼中,構(gòu)造函數(shù)Animal的prototype屬性,就是實(shí)例對(duì)象cat1和cat2的原型對(duì)象。原型對(duì)象上添加一個(gè)color屬性,結(jié)果,實(shí)例對(duì)象都共享了該屬性。原型對(duì)象的屬性不是實(shí)例對(duì)象自身的屬性。只要修改原型對(duì)象,變動(dòng)就立刻會(huì)體現(xiàn)在所有實(shí)例對(duì)象上。也就是說,當(dāng)實(shí)例對(duì)象本身沒有某個(gè)屬性或方法的時(shí)候,它會(huì)到原型對(duì)象去尋找該屬性或方法。這就是原型對(duì)象的特殊之處。如果實(shí)例對(duì)象自身就有某個(gè)屬性或方法,它就不會(huì)再去原型對(duì)象尋找這個(gè)屬性或方法
Animal.prototype.color = 'yellow'; cat1.color = 'black'; cat1.color // 'black' cat2.color // 'yellow' Animal.prototype.color // 'yellow';總結(jié)一下,原型對(duì)象的作用,就是定義所有實(shí)例對(duì)象共享的屬性和方法。這也是它被稱為原型對(duì)象的原因,而實(shí)例對(duì)象可以視作從原型對(duì)象衍生出來的子對(duì)象
Animal.prototype.walk = function () {console.log(this.name + ' is walking'); };上面代碼中,Animal.prototype對(duì)象上面定義了一個(gè)walk方法,這個(gè)方法將可以在所有Animal實(shí)例對(duì)象上面調(diào)用
原型鏈
JavaScript 規(guī)定,所有對(duì)象都有自己的原型對(duì)象(prototype)。一方面,任何一個(gè)對(duì)象,都可以充當(dāng)其他對(duì)象的原型;另一方面,由于原型對(duì)象也是對(duì)象,所以它也有自己的原型。因此,就會(huì)形成一個(gè)“原型鏈”(prototype chain):對(duì)象到原型,再到原型的原型……。如果一層層地上溯,所有對(duì)象的原型最終都可以上溯到Object.prototype,即Object構(gòu)造函數(shù)的prototype屬性。也就是說,所有對(duì)象都繼承了Object.prototype的屬性。這就是所有對(duì)象都有valueOf和toString方法的原因,因?yàn)檫@是從Object.prototype繼承的。那么,Object.prototype對(duì)象有沒有它的原型呢?回答是Object.prototype的原型是null。null沒有任何屬性和方法,也沒有自己的原型。因此,原型鏈的盡頭就是null
Object.getPrototypeOf(Object.prototype) // null上面代碼表示,Object.prototype對(duì)象的原型是null,由于null沒有任何屬性,所以原型鏈到此為止。Object.getPrototypeOf方法返回參數(shù)對(duì)象的原型。讀取對(duì)象的某個(gè)屬性時(shí),JavaScript 引擎先尋找對(duì)象本身的屬性,如果找不到,就到它的原型去找,如果還是找不到,就到原型的原型去找。如果直到最頂層的Object.prototype還是找不到,則返回undefined。如果對(duì)象自身和它的原型,都定義了一個(gè)同名屬性,那么優(yōu)先讀取對(duì)象自身的屬性,這叫做“覆蓋”(overriding)
注意,一級(jí)級(jí)向上,在整個(gè)原型鏈上尋找某個(gè)屬性,對(duì)性能是有影響的。所尋找的屬性在越上層的原型對(duì)象,對(duì)性能的影響越大。如果尋找某個(gè)不存在的屬性,將會(huì)遍歷整個(gè)原型鏈。舉例來說,如果讓構(gòu)造函數(shù)的prototype屬性指向一個(gè)數(shù)組,就意味著實(shí)例對(duì)象可以調(diào)用數(shù)組方法
var MyArray = function () {}; MyArray.prototype = new Array(); MyArray.prototype.constructor = MyArray; var mine = new MyArray(); mine.push(1, 2, 3); mine.length // 3 mine instanceof Array // true上面代碼中,mine是構(gòu)造函數(shù)MyArray的實(shí)例對(duì)象,由于MyArray.prototype指向一個(gè)數(shù)組實(shí)例,使得mine可以調(diào)用數(shù)組方法(這些方法定義在數(shù)組實(shí)例的prototype對(duì)象上面)。最后那行instanceof表達(dá)式,用來比較一個(gè)對(duì)象是否為某個(gè)構(gòu)造函數(shù)的實(shí)例,結(jié)果就是證明mine為Array的實(shí)例。上面代碼還出現(xiàn)了原型對(duì)象的constructor屬性
constructor 屬性
prototype對(duì)象有一個(gè)constructor屬性,默認(rèn)指向prototype對(duì)象所在的構(gòu)造函數(shù)
function P() {} P.prototype.constructor === P // true由于constructor屬性定義在prototype對(duì)象上面,意味著可以被所有實(shí)例對(duì)象繼承
function P() {} var p = new P(); p.constructor === P // true p.constructor === P.prototype.constructor // true p.hasOwnProperty('constructor') // false P.prototype.hasOwnProperty('constructor') // true上面代碼中,p是構(gòu)造函數(shù)P的實(shí)例對(duì)象,但是p自身沒有constructor屬性,該屬性其實(shí)是讀取原型鏈上面的P.prototype.constructor屬性。constructor屬性的作用是,可以得知某個(gè)實(shí)例對(duì)象,到底是哪一個(gè)構(gòu)造函數(shù)產(chǎn)生的
function F() {}; var f = new F(); f.constructor === F // true f.constructor === RegExp // false上面代碼中,constructor屬性確定了實(shí)例對(duì)象f的構(gòu)造函數(shù)是F,而不是RegExp。另一方面,有了constructor屬性,就可以從一個(gè)實(shí)例對(duì)象新建另一個(gè)實(shí)例
function Constr() {} var x = new Constr(); var y = new x.constructor(); y instanceof Constr // true上面代碼中,x是構(gòu)造函數(shù)Constr的實(shí)例,可以從x.constructor間接調(diào)用構(gòu)造函數(shù)。這使得在實(shí)例方法中,調(diào)用自身的構(gòu)造函數(shù)成為可能
Constr.prototype.createCopy = function () {return new this.constructor(); };上面代碼中,createCopy方法調(diào)用構(gòu)造函數(shù),新建另一個(gè)實(shí)例。constructor屬性表示原型對(duì)象與構(gòu)造函數(shù)之間的關(guān)聯(lián)關(guān)系,如果修改了原型對(duì)象,一般會(huì)同時(shí)修改constructor屬性,防止引用的時(shí)候出錯(cuò)
function Person(name) { this.name = name;} Person.prototype.constructor === Person // true Person.prototype = { method: function () {} }; Person.prototype.constructor === Person // false Person.prototype.constructor === Object // true上面代碼中,構(gòu)造函數(shù)Person的原型對(duì)象改掉了,但是沒有修改constructor屬性,導(dǎo)致這個(gè)屬性不再指向Person。由于Person的新原型是一個(gè)普通對(duì)象,而普通對(duì)象的constructor屬性指向Object構(gòu)造函數(shù),導(dǎo)致Person.prototype.constructor變成了Object。所以,修改原型對(duì)象時(shí),一般要同時(shí)修改constructor屬性的指向
// 壞的寫法 C.prototype = {method1: function (...) { ... }, }; // 好的寫法 C.prototype = {constructor: C,method1: function (...) { ... }, }; // 更好的寫法 C.prototype.method1 = function (...) { ... };上面代碼中,要么將constructor屬性重新指向原來的構(gòu)造函數(shù),要么只在原型對(duì)象上添加方法,這樣可以保證instanceof運(yùn)算符不會(huì)失真。如果不能確定constructor屬性是什么函數(shù),還有一個(gè)辦法:通過name屬性,從實(shí)例得到構(gòu)造函數(shù)的名稱
function Foo() {} var f = new Foo(); f.constructor.name // "Foo"instanceof 運(yùn)算符
instanceof運(yùn)算符返回一個(gè)布爾值,表示對(duì)象是否為某個(gè)構(gòu)造函數(shù)的實(shí)例
var v = new Vehicle(); v instanceof Vehicle // true上面代碼中,對(duì)象v是構(gòu)造函數(shù)Vehicle的實(shí)例,所以返回true。instanceof運(yùn)算符的左邊是實(shí)例對(duì)象,右邊是構(gòu)造函數(shù)。它會(huì)檢查右邊構(gòu)建函數(shù)的原型對(duì)象(prototype),是否在左邊對(duì)象的原型鏈上。因此,下面兩種寫法是等價(jià)的
v instanceof Vehicle // 等同于 Vehicle.prototype.isPrototypeOf(v)由于instanceof檢查整個(gè)原型鏈,因此同一個(gè)實(shí)例對(duì)象,可能會(huì)對(duì)多個(gè)構(gòu)造函數(shù)都返回true
var d = new Date(); d instanceof Date // true d instanceof Object // true上面代碼中,d同時(shí)是Date和Object的實(shí)例,因此對(duì)這兩個(gè)構(gòu)造函數(shù)都返回true。instanceof的原理是檢查右邊構(gòu)造函數(shù)的prototype屬性,是否在左邊對(duì)象的原型鏈上。有一種特殊情況,就是左邊對(duì)象的原型鏈上,只有null對(duì)象。這時(shí),instanceof判斷會(huì)失真
var obj = Object.create(null); typeof obj // "object" Object.create(null) instanceof Object // false上面代碼中,Object.create(null)返回一個(gè)新對(duì)象obj,它的原型是null。右邊的構(gòu)造函數(shù)Object的prototype屬性,不在左邊的原型鏈上,因此instanceof就認(rèn)為obj不是Object的實(shí)例。但是,只要一個(gè)對(duì)象的原型不是null,instanceof運(yùn)算符的判斷就不會(huì)失真。instanceof運(yùn)算符的一個(gè)用處,是判斷值的類型
var x = [1, 2, 3]; var y = {}; x instanceof Array // true y instanceof Object // true注意,instanceof運(yùn)算符只能用于對(duì)象,不適用原始類型的值。此外,對(duì)于undefined和null,instanceOf運(yùn)算符總是返回false
var s = 'hello'; s instanceof String // false undefined instanceof Object // false null instanceof Object // false利用instanceof運(yùn)算符,還可以巧妙地解決,調(diào)用構(gòu)造函數(shù)時(shí),忘了加new命令的問題
function Fubar (foo, bar) {if (this instanceof Fubar) {this._foo = foo;this._bar = bar;} else {return new Fubar(foo, bar);} }上面代碼使用instanceof運(yùn)算符,在函數(shù)體內(nèi)部判斷this關(guān)鍵字是否為構(gòu)造函數(shù)Fubar的實(shí)例。如果不是,就表明忘了加new命令
構(gòu)造函數(shù)的繼承
讓一個(gè)構(gòu)造函數(shù)繼承另一個(gè)構(gòu)造函數(shù),是非常常見的需求。這可以分成兩步實(shí)現(xiàn)
第一步:在子類的構(gòu)造函數(shù)中,調(diào)用父類的構(gòu)造函數(shù)
function Sub(value) {Super.call(this);this.prop = value; }上面代碼中,Sub是子類的構(gòu)造函數(shù),this是子類的實(shí)例。在實(shí)例上調(diào)用父類的構(gòu)造函數(shù)Super,就會(huì)讓子類實(shí)例具有父類實(shí)例的屬性。
第二步,是讓子類的原型指向父類的原型,這樣子類就可以繼承父類原型
Sub.prototype = Object.create(Super.prototype); Sub.prototype.constructor = Sub; Sub.prototype.method = '...';上面代碼中,Sub.prototype是子類的原型,要將它賦值為Object.create(Super.prototype),而不是直接等于Super.prototype。否則后面兩行對(duì)Sub.prototype的操作,會(huì)連父類的原型Super.prototype一起修改掉。
另外一種寫法是Sub.prototype等于一個(gè)父類實(shí)例
Sub.prototype = new Super();上面這種寫法也有繼承的效果,但是子類會(huì)具有父類實(shí)例的方法。有時(shí),這可能不是我們需要的,所以不推薦使用這種寫法。舉例來說,下面是一個(gè)Shape構(gòu)造函數(shù)
function Shape() { this.x = 0; this.y = 0;} Shape.prototype.move = function (x, y) {this.x += x;this.y += y;console.info('Shape moved.'); };我們需要讓Rectangle構(gòu)造函數(shù)繼承Shape
// 第一步,子類繼承父類的實(shí)例 function Rectangle() {Shape.call(this); // 調(diào)用父類構(gòu)造函數(shù) } // 另一種寫法 function Rectangle() {this.base = Shape;this.base(); } // 第二步,子類繼承父類的原型 Rectangle.prototype = Object.create(Shape.prototype); Rectangle.prototype.constructor = Rectangle;采用這樣的寫法以后,instanceof運(yùn)算符會(huì)對(duì)子類和父類的構(gòu)造函數(shù),都返回true
var rect = new Rectangle(); rect instanceof Rectangle // true rect instanceof Shape // true上面代碼中,子類是整體繼承父類。有時(shí)只需要單個(gè)方法的繼承,這時(shí)可以采用下面的寫法
ClassB.prototype.print = function() {ClassA.prototype.print.call(this);// some code }上面代碼中,子類B的print方法先調(diào)用父類A的print方法,再部署自己的代碼。這就等于繼承了父類A的print方法
多重繼承
JavaScript 不提供多重繼承功能,即不允許一個(gè)對(duì)象同時(shí)繼承多個(gè)對(duì)象。但是,可以通過變通方法,實(shí)現(xiàn)這個(gè)功能
function M1() { this.hello = 'hello';} function M2() { this.world = 'world';} function S() { M1.call(this); M2.call(this);} S.prototype = Object.create(M1.prototype); // 繼承 M1 Object.assign(S.prototype, M2.prototype); // 繼承鏈上加入 M2 S.prototype.constructor = S; // 指定構(gòu)造函數(shù) var s = new S(); s.hello // 'hello' s.world // 'world'上面代碼中,子類S同時(shí)繼承了父類M1和M2。這種模式又稱為 Mixin(混入)
模塊
隨著網(wǎng)站逐漸變成“互聯(lián)網(wǎng)應(yīng)用程序”,嵌入網(wǎng)頁的 JavaScript 代碼越來越龐大,越來越復(fù)雜。網(wǎng)頁越來越像桌面程序,需要一個(gè)團(tuán)隊(duì)分工協(xié)作、進(jìn)度管理、單元測(cè)試等等……開發(fā)者必須使用軟件工程的方法,管理網(wǎng)頁的業(yè)務(wù)邏輯。JavaScript 模塊化編程,已經(jīng)成為一個(gè)迫切的需求。理想情況下,開發(fā)者只需要實(shí)現(xiàn)核心的業(yè)務(wù)邏輯,其他都可以加載別人已經(jīng)寫好的模塊。但是,JavaScript 不是一種模塊化編程語言,ES6 才開始支持“類”和“模塊”。下面介紹傳統(tǒng)的做法,如何利用對(duì)象實(shí)現(xiàn)模塊的效果
基本的實(shí)現(xiàn)方法
模塊是實(shí)現(xiàn)特定功能的一組屬性和方法的封裝。簡(jiǎn)單的做法是把模塊寫成一個(gè)對(duì)象,所有的模塊成員都放到這個(gè)對(duì)象里面
var module1 = new Object({_count : 0,m1 : function (){//...},m2 : function (){//...} });上面的函數(shù)m1和m2,都封裝在module1對(duì)象里。使用的時(shí)候,就是調(diào)用這個(gè)對(duì)象的屬性
module1.m1();但是,這樣的寫法會(huì)暴露所有模塊成員,內(nèi)部狀態(tài)可以被外部改寫。比如,外部代碼可以直接改變內(nèi)部計(jì)數(shù)器的值
module1._count = 5;封裝私有變量:構(gòu)造函數(shù)的寫法
我們可以利用構(gòu)造函數(shù),封裝私有變量
function StringBuilder() {var buffer = [];this.add = function (str) {buffer.push(str);};this.toString = function () {return buffer.join('');}; }上面代碼中,buffer是模塊的私有變量。一旦生成實(shí)例對(duì)象,外部是無法直接訪問buffer的。但是,這種方法將私有變量封裝在構(gòu)造函數(shù)中,導(dǎo)致構(gòu)造函數(shù)與實(shí)例對(duì)象是一體的,總是存在于內(nèi)存之中,無法在使用完成后清除。這意味著,構(gòu)造函數(shù)有雙重作用,既用來塑造實(shí)例對(duì)象,又用來保存實(shí)例對(duì)象的數(shù)據(jù),違背了構(gòu)造函數(shù)與實(shí)例對(duì)象在數(shù)據(jù)上相分離的原則(即實(shí)例對(duì)象的數(shù)據(jù),不應(yīng)該保存在實(shí)例對(duì)象以外)。同時(shí),非常耗費(fèi)內(nèi)存
function StringBuilder() {this._buffer = []; } StringBuilder.prototype = {constructor: StringBuilder,add: function (str) {this._buffer.push(str);},toString: function () {return this._buffer.join('');} };這種方法將私有變量放入實(shí)例對(duì)象中,好處是看上去更自然,但是它的私有變量可以從外部讀寫,不是很安全
封裝私有變量:立即執(zhí)行函數(shù)的寫法
另一種做法是使用“立即執(zhí)行函數(shù)”(Immediately-Invoked Function Expression,IIFE),將相關(guān)的屬性和方法封裝在一個(gè)函數(shù)作用域里面,可以達(dá)到不暴露私有成員的目的
var module1 = (function () {var _count = 0;var m1 = function () {//...};var m2 = function () {//...};return {m1 : m1, m2 : m2}; })();使用上面的寫法,外部代碼無法讀取內(nèi)部的_count變量
console.info(module1._count); //undefined上面的module1就是 JavaScript 模塊的基本寫法。下面,再對(duì)這種寫法進(jìn)行加工
模塊的放大模式
如果一個(gè)模塊很大,必須分成幾個(gè)部分,或者一個(gè)模塊需要繼承另一個(gè)模塊,這時(shí)就有必要采用“放大模式”(augmentation)
var module1 = (function (mod){mod.m3 = function () {//...};return mod; })(module1);上面的代碼為module1模塊添加了一個(gè)新方法m3(),然后返回新的module1模塊。在瀏覽器環(huán)境中,模塊的各個(gè)部分通常都是從網(wǎng)上獲取的,有時(shí)無法知道哪個(gè)部分會(huì)先加載。如果采用上面的寫法,第一個(gè)執(zhí)行的部分有可能加載一個(gè)不存在空對(duì)象,這時(shí)就要采用"寬放大模式"(Loose augmentation)
var module1 = (function (mod) {//...return mod; })(window.module1 || {});與"放大模式"相比,“寬放大模式”就是“立即執(zhí)行函數(shù)”的參數(shù)可以是空對(duì)象
輸入全局變量
獨(dú)立性是模塊的重要特點(diǎn),模塊內(nèi)部最好不與程序的其他部分直接交互。為了在模塊內(nèi)部調(diào)用全局變量,必須顯式地將其他變量輸入模塊
var module1 = (function ($, YAHOO) {//... })(jQuery, YAHOO);上面的module1模塊需要使用 jQuery 庫和 YUI 庫,就把這兩個(gè)庫(其實(shí)是兩個(gè)模塊)當(dāng)作參數(shù)輸入module1。這樣做除了保證模塊的獨(dú)立性,還使得模塊之間的依賴關(guān)系變得明顯。立即執(zhí)行函數(shù)還可以起到命名空間的作用
(function($, window, document) {function go(num) {}function handleEvents() {}function initialize() {}function dieCarouselDie() {}//attach to the global scopewindow.finalCarousel = {init : initialize,destroy : dieCarouselDie} })( jQuery, window, document );上面代碼中,finalCarousel對(duì)象輸出到全局,對(duì)外暴露init和destroy接口,內(nèi)部方法go、handleEvents、initialize、dieCarouselDie都是外部無法調(diào)用的
Object 對(duì)象的相關(guān)方法
JavaScript 在Object對(duì)象上面,提供了很多相關(guān)方法,處理面向?qū)ο缶幊痰南嚓P(guān)操作
Object.getPrototypeOf()
Object.getPrototypeOf方法返回參數(shù)對(duì)象的原型。這是獲取原型對(duì)象的標(biāo)準(zhǔn)方法
var F = function () {}; var f = new F(); Object.getPrototypeOf(f) === F.prototype // true上面代碼中,實(shí)例對(duì)象f的原型是F.prototype。下面是幾種特殊對(duì)象的原型
// 空對(duì)象的原型是 Object.prototype Object.getPrototypeOf({}) === Object.prototype // true // Object.prototype 的原型是 null Object.getPrototypeOf(Object.prototype) === null // true // 函數(shù)的原型是 Function.prototype function f() {} Object.getPrototypeOf(f) === Function.prototype // trueObject.setPrototypeOf()
Object.setPrototypeOf方法為參數(shù)對(duì)象設(shè)置原型,返回該參數(shù)對(duì)象。它接受兩個(gè)參數(shù),第一個(gè)是現(xiàn)有對(duì)象,第二個(gè)是原型對(duì)象
var a = {}; var b = {x: 1}; Object.setPrototypeOf(a, b); Object.getPrototypeOf(a) === b // true a.x // 1上面代碼中,Object.setPrototypeOf方法將對(duì)象a的原型,設(shè)置為對(duì)象b,因此a可以共享b的屬性。new命令可以使用Object.setPrototypeOf方法模擬
var F = function () {this.foo = 'bar'; }; var f = new F(); // 等同于 var f = Object.setPrototypeOf({}, F.prototype); F.call(f);上面代碼中,new命令新建實(shí)例對(duì)象,其實(shí)可以分成兩步。第一步,將一個(gè)空對(duì)象的原型設(shè)為構(gòu)造函數(shù)的prototype屬性(上例是F.prototype);第二步,將構(gòu)造函數(shù)內(nèi)部的this綁定這個(gè)空對(duì)象,然后執(zhí)行構(gòu)造函數(shù),使得定義在this上面的方法和屬性(上例是this.foo),都轉(zhuǎn)移到這個(gè)空對(duì)象上
Object.create()
生成實(shí)例對(duì)象的常用方法是使用new命令讓構(gòu)造函數(shù)返回一個(gè)實(shí)例。但是很多時(shí)候只能拿到一個(gè)實(shí)例對(duì)象,它可能根本不是由構(gòu)建函數(shù)生成的,那么能不能從一個(gè)實(shí)例對(duì)象生成另一個(gè)實(shí)例對(duì)象呢?JavaScript 提供了Object.create方法,用來滿足這種需求。該方法接受一個(gè)對(duì)象作為參數(shù),然后以它為原型,返回一個(gè)實(shí)例對(duì)象。該實(shí)例完全繼承原型對(duì)象的屬性
// 原型對(duì)象 var A = {print: function () { console.log('hello'); } }; // 實(shí)例對(duì)象 var B = Object.create(A); Object.getPrototypeOf(B) === A // true B.print() // hello B.print === A.print // true上面代碼中,Object.create方法以A對(duì)象為原型,生成了B對(duì)象。B繼承了A的所有屬性和方法。實(shí)際上,Object.create方法可以用下面的代碼代替
if (typeof Object.create !== 'function') {Object.create = function (obj) {function F() {}F.prototype = obj;return new F();}; }上面代碼表明,Object.create方法的實(shí)質(zhì)是新建一個(gè)空的構(gòu)造函數(shù)F,然后讓F.prototype屬性指向參數(shù)對(duì)象obj,最后返回一個(gè)F的實(shí)例,從而實(shí)現(xiàn)讓該實(shí)例繼承obj的屬性。下面三種方式生成的新對(duì)象是等價(jià)的
var obj1 = Object.create({}); var obj2 = Object.create(Object.prototype); var obj3 = new Object();如果想要生成一個(gè)不繼承任何屬性(比如沒有toString和valueOf方法)的對(duì)象,可以將Object.create的參數(shù)設(shè)為null
var obj = Object.create(null); obj.valueOf() // TypeError: Object [object Object] has no method 'valueOf'上面代碼中,對(duì)象obj的原型是null,它就不具備一些定義在Object.prototype對(duì)象上面的屬性,比如valueOf方法。使用Object.create方法的時(shí)候,必須提供對(duì)象原型,即參數(shù)不能為空,或者不是對(duì)象,否則會(huì)報(bào)錯(cuò)
Object.create() // TypeError: Object prototype may only be an Object or null Object.create(123) // TypeError: Object prototype may only be an Object or nullObject.create方法生成的新對(duì)象,動(dòng)態(tài)繼承了原型。在原型上添加或修改任何方法,會(huì)立刻反映在新對(duì)象之上
var obj1 = { p: 1 }; var obj2 = Object.create(obj1); obj1.p = 2; obj2.p // 2上面代碼中,修改對(duì)象原型obj1會(huì)影響到實(shí)例對(duì)象obj2。除了對(duì)象的原型,Object.create方法還可以接受第二個(gè)參數(shù)。該參數(shù)是一個(gè)屬性描述對(duì)象,它所描述的對(duì)象屬性,會(huì)添加到實(shí)例對(duì)象,作為該對(duì)象自身的屬性
var obj = Object.create({}, {p1: { value: 123, enumerable: true, configurable: true, writable: true,},p2: { value: 'abc', enumerable: true, configurable: true, writable: true,} }); // 等同于 var obj = Object.create({}); obj.p1 = 123; obj.p2 = 'abc';Object.create方法生成的對(duì)象,繼承了它的原型對(duì)象的構(gòu)造函數(shù)
function A() {} var a = new A(); var b = Object.create(a); b.constructor === A // true b instanceof A // true上面代碼中,b對(duì)象的原型是a對(duì)象,因此繼承了a對(duì)象的構(gòu)造函數(shù)A
Object.prototype.isPrototypeOf()
實(shí)例對(duì)象的isPrototypeOf方法,用來判斷該對(duì)象是否為參數(shù)對(duì)象的原型
var o1 = {}; var o2 = Object.create(o1); var o3 = Object.create(o2); o2.isPrototypeOf(o3) // true o1.isPrototypeOf(o3) // true上面代碼中,o1和o2都是o3的原型。這表明只要實(shí)例對(duì)象處在參數(shù)對(duì)象的原型鏈上,isPrototypeOf方法都返回true
Object.prototype.isPrototypeOf({}) // true Object.prototype.isPrototypeOf([]) // true Object.prototype.isPrototypeOf(/xyz/) // true Object.prototype.isPrototypeOf(Object.create(null)) // false上面代碼中,由于Object.prototype處于原型鏈的最頂端,所以對(duì)各種實(shí)例都返回true,只有直接繼承自null的對(duì)象除外
Object.prototype.__proto__
實(shí)例對(duì)象的__proto__屬性(前后各兩個(gè)下劃線),返回該對(duì)象的原型。該屬性可讀寫
var obj = {}; var p = {}; obj.__proto__ = p; Object.getPrototypeOf(obj) === p // true上面代碼通過__proto__屬性,將p對(duì)象設(shè)為obj對(duì)象的原型。根據(jù)語言標(biāo)準(zhǔn),__proto__屬性只有瀏覽器才需要部署,其他環(huán)境可以沒有這個(gè)屬性。它前后的兩根下劃線,表明它本質(zhì)是一個(gè)內(nèi)部屬性,不應(yīng)該對(duì)使用者暴露;因此,應(yīng)該盡量少用這個(gè)屬性,而是用Object.getPrototypeOf()和Object.setPrototypeOf(),進(jìn)行原型對(duì)象的讀寫操作。原型鏈可以用__proto__很直觀地表示
var A = { name: '張三'}; var B = { name: '李四'}; var proto = {print: function () { console.log(this.name); } }; A.__proto__ = proto; B.__proto__ = proto; A.print() // 張三 B.print() // 李四 A.print === B.print // true A.print === proto.print // true B.print === proto.print // true上面代碼中,A對(duì)象和B對(duì)象的原型都是proto對(duì)象,它們都共享proto對(duì)象的print方法。也就是說,A和B的print方法,都是在調(diào)用proto對(duì)象的print方法
獲取原型對(duì)象方法的比較
如前所述,__proto__屬性指向當(dāng)前對(duì)象的原型對(duì)象,即構(gòu)造函數(shù)的prototype屬性
var obj = new Object(); obj.__proto__ === Object.prototype // true obj.__proto__ === obj.constructor.prototype // true上面代碼首先新建了一個(gè)對(duì)象obj,它的__proto__屬性,指向構(gòu)造函數(shù)(Object或obj.constructor)的prototype屬性;因此,獲取實(shí)例對(duì)象obj的原型對(duì)象,有三種方法
obj.__proto__ obj.constructor.prototype Object.getPrototypeOf(obj)上面三種方法之中,前兩種都不是很可靠。__proto__屬性只有瀏覽器才需要部署,其他環(huán)境可以不部署。而obj.constructor.prototype在手動(dòng)改變?cè)蛯?duì)象時(shí),可能會(huì)失效
var P = function () {}; var p = new P(); var C = function () {}; C.prototype = p; var c = new C(); c.constructor.prototype === p // false上面代碼中,構(gòu)造函數(shù)C的原型對(duì)象被改成了p,但是實(shí)例對(duì)象的c.constructor.prototype卻沒有指向p。所以,在改變?cè)蛯?duì)象時(shí),一般要同時(shí)設(shè)置constructor屬性
C.prototype = p; C.prototype.constructor = C; var c = new C(); c.constructor.prototype === p // true因此,推薦使用第三種Object.getPrototypeOf方法,獲取原型對(duì)象
Object.getOwnPropertyNames()
Object.getOwnPropertyNames方法返回一個(gè)數(shù)組,成員是參數(shù)對(duì)象本身的所有屬性的鍵名,不包含繼承的屬性鍵名
Object.getOwnPropertyNames(Date) // ["length", "name", "prototype", "now", "parse", "UTC"]上面代碼中,Object.getOwnPropertyNames方法返回Date所有自身的屬性名。對(duì)象本身的屬性之中,有的是可以遍歷的(enumerable)有的是不可以遍歷的。Object.getOwnPropertyNames方法返回所有鍵名,不管是否可以遍歷。只獲取那些可以遍歷的屬性,使用Object.keys方法
Object.keys(Date) // []上面代碼表明,Date對(duì)象所有自身的屬性,都是不可以遍歷的
Object.prototype.hasOwnProperty()
對(duì)象實(shí)例的hasOwnProperty方法返回一個(gè)布爾值,用于判斷某個(gè)屬性定義在對(duì)象自身,還是定義在原型鏈上
Date.hasOwnProperty('length') // true Date.hasOwnProperty('toString') // false上面代碼表明,Date.length(構(gòu)造函數(shù)Date可以接受多少個(gè)參數(shù))是Date自身的屬性,Date.toString是繼承的屬性。另外,hasOwnProperty方法是 JavaScript 之中唯一一個(gè)處理對(duì)象屬性時(shí),不會(huì)遍歷原型鏈的方法
in 運(yùn)算符和 for...in 循環(huán)
in運(yùn)算符返回一個(gè)布爾值,表示一個(gè)對(duì)象是否具有某個(gè)屬性。它不區(qū)分該屬性是對(duì)象自身的屬性,還是繼承的屬性
'length' in Date // true 'toString' in Date // truein運(yùn)算符常用于檢查一個(gè)屬性是否存在。獲得對(duì)象的所有可遍歷屬性(不管是自身的還是繼承的),可以使用for...in循環(huán)
var o1 = { p1: 123 }; var o2 = Object.create(o1, {p2: { value: "abc", enumerable: true } }); for (p in o2) { console.info(p);} // p2 // p1上面代碼中,對(duì)象o2的p2屬性是自身的,p1屬性是繼承的。這兩個(gè)屬性都會(huì)被for...in循環(huán)遍歷。為了在for...in循環(huán)中獲得對(duì)象自身的屬性,可以采用hasOwnProperty方法判斷一下
for ( var name in object ) {if ( object.hasOwnProperty(name) ) {/* loop code */} }獲得對(duì)象的所有屬性(不管是自身的還是繼承的,也不管是否可枚舉),可以使用下面的函數(shù)
function inheritedPropertyNames(obj) {var props = {};while(obj) {Object.getOwnPropertyNames(obj).forEach(function(p) { props[p] = true;});obj = Object.getPrototypeOf(obj);}return Object.getOwnPropertyNames(props); }上面代碼依次獲取obj對(duì)象的每一級(jí)原型對(duì)象“自身”的屬性,從而獲取obj對(duì)象的“所有”屬性,不管是否可遍歷。下面是一個(gè)例子,列出Date對(duì)象的所有屬性
inheritedPropertyNames(Date) // ["caller","constructor","toString","UTC",...]對(duì)象的拷貝
如果要拷貝一個(gè)對(duì)象,需要做到下面兩件事情。
1.確保拷貝后的對(duì)象,與原對(duì)象具有同樣的原型。
2.確保拷貝后的對(duì)象,與原對(duì)象具有同樣的實(shí)例屬性。
下面就是根據(jù)上面兩點(diǎn),實(shí)現(xiàn)的對(duì)象拷貝函數(shù)
function copyObject(orig) {var copy = Object.create(Object.getPrototypeOf(orig));copyOwnPropertiesFrom(copy, orig);return copy; } function copyOwnPropertiesFrom(target, source) {Object.getOwnPropertyNames(source).forEach(function (propKey) {var desc = Object.getOwnPropertyDescriptor(source, propKey);Object.defineProperty(target, propKey, desc);});return target; }另一種更簡(jiǎn)單的寫法,是利用 ES2017 才引入標(biāo)準(zhǔn)的Object.getOwnPropertyDescriptors方法
function copyObject(orig) {return Object.create(Object.getPrototypeOf(orig),Object.getOwnPropertyDescriptors(orig)); }嚴(yán)格模式
除了正常的運(yùn)行模式,JavaScript 還有第二種運(yùn)行模式:嚴(yán)格模式(strict mode)。顧名思義,這種模式采用更加嚴(yán)格的 JavaScript 語法。同樣的代碼,在正常模式和嚴(yán)格模式中,可能會(huì)有不一樣的運(yùn)行結(jié)果。一些在正常模式下可以運(yùn)行的語句,在嚴(yán)格模式下將不能運(yùn)行
設(shè)計(jì)目的
早期的 JavaScript 語言有很多設(shè)計(jì)不合理的地方,但是為了兼容以前的代碼,又不能改變老的語法,只能不斷添加新的語法,引導(dǎo)程序員使用新語法。
嚴(yán)格模式是從 ES5 進(jìn)入標(biāo)準(zhǔn)的,主要目的有以下幾個(gè):
1.明確禁止一些不合理、不嚴(yán)謹(jǐn)?shù)恼Z法,減少 JavaScript 語言的一些怪異行為
2.增加更多報(bào)錯(cuò)的場(chǎng)合,消除代碼運(yùn)行的一些不安全之處,保證代碼運(yùn)行的安全
3.提高編譯器效率,增加運(yùn)行速度
4.為未來新版本的 JavaScript 語法做好鋪墊
總之,嚴(yán)格模式體現(xiàn)了 JavaScript 更合理、更安全、更嚴(yán)謹(jǐn)?shù)陌l(fā)展方向
啟用方法
進(jìn)入嚴(yán)格模式的標(biāo)志,是一行字符串'use strict'
老版本的引擎會(huì)把它當(dāng)作一行普通字符串,加以忽略;新版本的引擎就會(huì)進(jìn)入嚴(yán)格模式。嚴(yán)格模式可以用于整個(gè)腳本,也可以只用于單個(gè)函數(shù)
整個(gè)腳本文件
use strict放在腳本文件的第一行,整個(gè)腳本都將以嚴(yán)格模式運(yùn)行;如果這行語句不在第一行就無效,整個(gè)腳本會(huì)以正常模式運(yùn)行。(嚴(yán)格地說,只要前面不是產(chǎn)生實(shí)際運(yùn)行結(jié)果的語句,use strict可以不在第一行,比如直接跟在一個(gè)空的分號(hào)后面,或者跟在注釋后面)
<script>'use strict';console.log('這是嚴(yán)格模式'); </script><script>console.log('這是正常模式'); </script>上面代碼中,一個(gè)網(wǎng)頁文件依次有兩段 JavaScript 代碼。前一個(gè)
<script>console.log('這是正常模式');'use strict'; </script>單個(gè)函數(shù)
use strict放在函數(shù)體的第一行,則整個(gè)函數(shù)以嚴(yán)格模式運(yùn)行
function strict() {'use strict';return '這是嚴(yán)格模式'; }function strict2() {'use strict';function f() {return '這也是嚴(yán)格模式';}return f(); }function notStrict() {return '這是正常模式'; }有時(shí)需要把不同腳本合并在一個(gè)文件里面。如果一個(gè)腳本是嚴(yán)格模式,另一個(gè)腳本不是,它們的合并就可能出錯(cuò)。嚴(yán)格模式的腳本在前,則合并后的腳本都是嚴(yán)格模式;如果正常模式的腳本在前,則合并后的腳本都是正常模式。這兩種情況下,合并后的結(jié)果都是不正確的。這時(shí)可以考慮把整個(gè)腳本文件放在一個(gè)立即執(zhí)行的匿名函數(shù)之中
(function () {'use strict';// some code here })();顯式報(bào)錯(cuò)
嚴(yán)格模式使得 JavaScript 的語法變得更嚴(yán)格,更多的操作會(huì)顯式報(bào)錯(cuò)。其中有些操作,在正常模式下只會(huì)默默地失敗,不會(huì)報(bào)錯(cuò)
只讀屬性不可寫
嚴(yán)格模式下,設(shè)置字符串的length屬性,會(huì)報(bào)錯(cuò)
'use strict'; 'abc'.length = 5; // TypeError: Cannot assign to read only property 'length' of string 'abc'上面代碼報(bào)錯(cuò),因?yàn)閘ength是只讀屬性,嚴(yán)格模式下不可寫。正常模式下,改變length屬性是無效的,但不會(huì)報(bào)錯(cuò)。嚴(yán)格模式下,對(duì)只讀屬性賦值,或者刪除不可配置(non-configurable)屬性都會(huì)報(bào)錯(cuò)
// 對(duì)只讀屬性賦值會(huì)報(bào)錯(cuò) 'use strict'; Object.defineProperty({}, 'a', {value: 37,writable: false }); obj.a = 123; // TypeError: Cannot assign to read only property 'a' of object #<Object>// 刪除不可配置的屬性會(huì)報(bào)錯(cuò) 'use strict'; var obj = Object.defineProperty({}, 'p', {value: 1,configurable: false }); delete obj.p // TypeError: Cannot delete property 'p' of #<Object>只設(shè)置了取值器的屬性不可寫
嚴(yán)格模式下,對(duì)一個(gè)只有取值器(getter)、沒有存值器(setter)的屬性賦值,會(huì)報(bào)錯(cuò)
'use strict'; var obj = {get v() { return 1; } }; obj.v = 2; // Uncaught TypeError: Cannot set property v of #<Object> which has only a getter禁止擴(kuò)展的對(duì)象不可擴(kuò)展
嚴(yán)格模式下,對(duì)禁止擴(kuò)展的對(duì)象添加新屬性,會(huì)報(bào)錯(cuò)
'use strict'; var obj = {}; Object.preventExtensions(obj); obj.v = 1; // Uncaught TypeError: Cannot add property v, object is not extensible上面代碼中,obj對(duì)象禁止擴(kuò)展,添加屬性就會(huì)報(bào)錯(cuò)
eval、arguments 不可用作標(biāo)識(shí)名
嚴(yán)格模式下,使用eval或者arguments作為標(biāo)識(shí)名,將會(huì)報(bào)錯(cuò)。下面的語句都會(huì)報(bào)錯(cuò)
'use strict'; var eval = 17; var arguments = 17; var obj = { set p(arguments) { } }; try { } catch (arguments) { } function x(eval) { } function arguments() { } var y = function eval() { }; var f = new Function('arguments', "'use strict'; return 17;"); // SyntaxError: Unexpected eval or arguments in strict mode函數(shù)不能有重名的參數(shù)
正常模式下,如果函數(shù)有多個(gè)重名的參數(shù),可以用arguments[i]讀取。嚴(yán)格模式下,這屬于語法錯(cuò)誤
function f(a, a, b) {'use strict';return a + b; } // Uncaught SyntaxError: Duplicate parameter name not allowed in this context禁止八進(jìn)制的前綴0表示法
正常模式下,整數(shù)的第一位如果是0,表示這是八進(jìn)制數(shù),比如0100等于十進(jìn)制的64。嚴(yán)格模式禁止這種表示法,整數(shù)第一位為0,將報(bào)錯(cuò)
增強(qiáng)的安全措施
嚴(yán)格模式增強(qiáng)了安全保護(hù),從語法上防止了一些不小心會(huì)出現(xiàn)的錯(cuò)誤
全局變量顯式聲明
正常模式中,如果一個(gè)變量沒有聲明就賦值,默認(rèn)是全局變量。嚴(yán)格模式禁止這種用法,全局變量必須顯式聲明
'use strict'; v = 1; // 報(bào)錯(cuò),v未聲明 for (i = 0; i < 2; i++) { // 報(bào)錯(cuò),i 未聲明// ... } function f() {x = 123; } f() // 報(bào)錯(cuò),未聲明就創(chuàng)建一個(gè)全局變量因此,嚴(yán)格模式下,變量都必須先聲明,然后再使用
禁止 this 關(guān)鍵字指向全局對(duì)象
正常模式下,函數(shù)內(nèi)部的this可能會(huì)指向全局對(duì)象,嚴(yán)格模式禁止這種用法,避免無意間創(chuàng)造全局變量
// 正常模式 function f() {console.log(this === window); } f() // true// 嚴(yán)格模式 function f() {'use strict';console.log(this === undefined); } f() // true上面代碼中,嚴(yán)格模式的函數(shù)體內(nèi)部this是undefined,這種限制對(duì)于構(gòu)造函數(shù)尤其有用。使用構(gòu)造函數(shù)時(shí),有時(shí)忘了加new,這時(shí)this不再指向全局對(duì)象,而是報(bào)錯(cuò)
function f() {'use strict';this.a = 1; }; f();// 報(bào)錯(cuò),this 未定義嚴(yán)格模式下,函數(shù)直接調(diào)用時(shí)(不使用new調(diào)用),函數(shù)內(nèi)部的this表示undefined(未定義),因此可以用call、apply和bind方法,將任意值綁定在this上面。正常模式下,this指向全局對(duì)象,如果綁定的值是非對(duì)象,將被自動(dòng)轉(zhuǎn)為對(duì)象再綁定上去,而null和undefined這兩個(gè)無法轉(zhuǎn)成對(duì)象的值,將被忽略
// 正常模式 function fun() { return this; } fun() // window fun.call(2) // Number {2} fun.call(true) // Boolean {true} fun.call(null) // window fun.call(undefined) // window// 嚴(yán)格模式 'use strict'; function fun() { return this; } fun() //undefined fun.call(2) // 2 fun.call(true) // true fun.call(null) // null fun.call(undefined) // undefined上面代碼中,可以把任意類型的值,綁定在this上面
禁止使用 fn.callee、fn.caller
函數(shù)內(nèi)部不得使用fn.caller、fn.arguments,否則會(huì)報(bào)錯(cuò)。這意味著不能在函數(shù)內(nèi)部得到調(diào)用棧了
function f1() {'use strict';f1.caller; // 報(bào)錯(cuò)f1.arguments; // 報(bào)錯(cuò) } f1();禁止使用 arguments.callee、arguments.caller
arguments.callee和arguments.caller是兩個(gè)歷史遺留的變量,從來沒有標(biāo)準(zhǔn)化過,現(xiàn)在已經(jīng)取消了。正常模式下調(diào)用它們沒有什么作用,但是不會(huì)報(bào)錯(cuò)。嚴(yán)格模式明確規(guī)定,函數(shù)內(nèi)部使用arguments.callee、arguments.caller將會(huì)報(bào)錯(cuò)
'use strict'; var f = function () { return arguments.callee; }; f(); // 報(bào)錯(cuò)禁止刪除變量
嚴(yán)格模式下無法刪除變量,如果使用delete命令刪除一個(gè)變量,會(huì)報(bào)錯(cuò)。只有對(duì)象的屬性,且屬性的描述對(duì)象的configurable屬性設(shè)置為true,才能被delete命令刪除
'use strict'; var x; delete x; // 語法錯(cuò)誤var obj = Object.create(null, {x: { value: 1, configurable: true } }); delete obj.x; // 刪除成功靜態(tài)綁定
JavaScript 語言的一個(gè)特點(diǎn),就是允許“動(dòng)態(tài)綁定”,即某些屬性和方法到底屬于哪一個(gè)對(duì)象,不是在編譯時(shí)確定的,而是在運(yùn)行時(shí)(runtime)確定的。嚴(yán)格模式對(duì)動(dòng)態(tài)綁定做了一些限制;某些情況下,只允許靜態(tài)綁定;也就是說,屬性和方法到底歸屬哪個(gè)對(duì)象,必須在編譯階段就確定。這樣做有利于編譯效率的提高,也使得代碼更容易閱讀,更少出現(xiàn)意外。具體來說,涉及以下幾個(gè)方面:
禁止使用 with 語句
嚴(yán)格模式下,使用with語句將報(bào)錯(cuò);因?yàn)閣ith語句無法在編譯時(shí)就確定,某個(gè)屬性到底歸屬哪個(gè)對(duì)象,從而影響了編譯效果
'use strict'; var v = 1; var obj = {}; with (obj) { v = 2; } // Uncaught SyntaxError: Strict mode code may not include a with statement創(chuàng)設(shè) eval 作用域
正常模式下,JavaScript 語言有兩種變量作用域(scope):全局作用域和函數(shù)作用域。嚴(yán)格模式創(chuàng)設(shè)了第三種作用域:eval作用域。正常模式下,eval語句的作用域取決于它處于全局作用域還是函數(shù)作用域。嚴(yán)格模式下,eval語句本身就是一個(gè)作用域,不再能夠在其所運(yùn)行的作用域創(chuàng)設(shè)新的變量了,也就是說,eval所生成的變量只能用于eval內(nèi)部
(function () {'use strict';var x = 2;console.log(eval('var x = 5; x')) // 5console.log(x) // 2 })()上面代碼中,由于eval語句內(nèi)部是一個(gè)獨(dú)立作用域,所以內(nèi)部的變量x不會(huì)泄露到外部。注意,如果希望eval語句也使用嚴(yán)格模式,有兩種方式
// 方式一 function f1(str){'use strict';return eval(str); } f1('undeclared_variable = 1'); // 報(bào)錯(cuò)// 方式二 function f2(str){ return eval(str); } f2('"use strict";undeclared_variable = 1') // 報(bào)錯(cuò)上面兩種寫法,eval內(nèi)部使用的都是嚴(yán)格模式
arguments 不再追蹤參數(shù)的變化
變量arguments代表函數(shù)的參數(shù)。嚴(yán)格模式下,函數(shù)內(nèi)部改變參數(shù)與arguments的聯(lián)系被切斷了,兩者不再存在聯(lián)動(dòng)關(guān)系
function f(a) {a = 2;return [a, arguments[0]]; } f(1); // 正常模式為[2, 2]function f(a) {'use strict';a = 2;return [a, arguments[0]]; } f(1); // 嚴(yán)格模式為[2, 1]上面代碼中,改變函數(shù)的參數(shù),不會(huì)反應(yīng)到arguments對(duì)象上來
向下一個(gè)版本的 JavaScript 過渡
JavaScript 語言的下一個(gè)版本是 ECMAScript 6,為了平穩(wěn)過渡,嚴(yán)格模式引入了一些 ES6 語法
非函數(shù)代碼塊不得聲明函數(shù)
ES6 會(huì)引入塊級(jí)作用域。為了與新版本接軌,ES5 的嚴(yán)格模式只允許在全局作用域或函數(shù)作用域聲明函數(shù)。也就是說,不允許在非函數(shù)的代碼塊內(nèi)聲明函數(shù)
'use strict'; if (true) {function f1() { } // 語法錯(cuò)誤 } for (var i = 0; i < 5; i++) {function f2() { } // 語法錯(cuò)誤 }上面代碼在if代碼塊和for代碼塊中聲明了函數(shù),ES5 環(huán)境會(huì)報(bào)錯(cuò)
注意,如果是 ES6 環(huán)境,上面的代碼不會(huì)報(bào)錯(cuò),因?yàn)?ES6 允許在代碼塊之中聲明函數(shù)
保留字
為了向?qū)?JavaScript 的新版本過渡,嚴(yán)格模式新增了一些保留字(implements、interface、let、package、private、protected、public、static、yield等)。使用這些詞作為變量名將會(huì)報(bào)錯(cuò)
function package(protected) { // 語法錯(cuò)誤'use strict';var implements; // 語法錯(cuò)誤 }異步操作
異步操作概述
單線程模型
單線程模型指的是,JavaScript 只在一個(gè)線程上運(yùn)行;也就是說,JavaScript 同時(shí)只能執(zhí)行一個(gè)任務(wù),其他任務(wù)都必須在后面排隊(duì)等待;注意,JavaScript 只在一個(gè)線程上運(yùn)行,不代表 JavaScript 引擎只有一個(gè)線程。事實(shí)上,JavaScript 引擎有多個(gè)線程,單個(gè)腳本只能在一個(gè)線程上運(yùn)行(稱為主線程),其他線程都是在后臺(tái)配合
JavaScript 之所以采用單線程,而不是多線程,跟歷史有關(guān)系;JavaScript 從誕生起就是單線程,原因是不想讓瀏覽器變得太復(fù)雜,因?yàn)槎嗑€程需要共享資源、且有可能修改彼此的運(yùn)行結(jié)果,對(duì)于一種網(wǎng)頁腳本語言來說就太復(fù)雜了。如果 JavaScript 同時(shí)有兩個(gè)線程,一個(gè)線程在網(wǎng)頁 DOM 節(jié)點(diǎn)上添加內(nèi)容,另一個(gè)線程刪除了這個(gè)節(jié)點(diǎn),這時(shí)瀏覽器應(yīng)該以哪個(gè)線程為準(zhǔn)?是不是還要有鎖機(jī)制?所以,為了避免復(fù)雜性,JavaScript 一開始就是單線程,這已經(jīng)成了這門語言的核心特征,將來也不會(huì)改變
這種模式的好處是實(shí)現(xiàn)起來比較簡(jiǎn)單,執(zhí)行環(huán)境相對(duì)單純;壞處是只要有一個(gè)任務(wù)耗時(shí)很長(zhǎng),后面的任務(wù)都必須排隊(duì)等著,會(huì)拖延整個(gè)程序的執(zhí)行。常見的瀏覽器無響應(yīng)(假死),往往就是因?yàn)槟骋欢?JavaScript 代碼長(zhǎng)時(shí)間運(yùn)行(比如死循環(huán)),導(dǎo)致整個(gè)頁面卡在這個(gè)地方,其他任務(wù)無法執(zhí)行。JavaScript 語言本身并不慢,慢的是讀寫外部數(shù)據(jù),比如等待 Ajax 請(qǐng)求返回結(jié)果;這個(gè)時(shí)候,如果對(duì)方服務(wù)器遲遲沒有響應(yīng),或者網(wǎng)絡(luò)不通暢,就會(huì)導(dǎo)致腳本的長(zhǎng)時(shí)間停滯
如果排隊(duì)是因?yàn)橛?jì)算量大,CPU 忙不過來,倒也算了,但是很多時(shí)候 CPU 是閑著的,因?yàn)?IO 操作(輸入輸出)很慢(比如 Ajax 操作從網(wǎng)絡(luò)讀取數(shù)據(jù)),不得不等著結(jié)果出來,再往下執(zhí)行。JavaScript 語言的設(shè)計(jì)者意識(shí)到,這時(shí) CPU 完全可以不管 IO 操作,掛起處于等待中的任務(wù),先運(yùn)行排在后面的任務(wù)。等到 IO 操作返回了結(jié)果,再回過頭,把掛起的任務(wù)繼續(xù)執(zhí)行下去。這種機(jī)制就是 JavaScript 內(nèi)部采用的“事件循環(huán)”機(jī)制(Event Loop)
單線程模型雖然對(duì) JavaScript 構(gòu)成了很大的限制,但也因此使它具備了其他語言不具備的優(yōu)勢(shì)。如果用得好,JavaScript 程序是不會(huì)出現(xiàn)堵塞的,這就是為什么 Node 可以用很少的資源,應(yīng)付大流量訪問的原因。為了利用多核 CPU 的計(jì)算能力,HTML5 提出 Web Worker 標(biāo)準(zhǔn),允許 JavaScript 腳本創(chuàng)建多個(gè)線程,但是子線程完全受主線程控制,且不得操作 DOM。所以,這個(gè)新標(biāo)準(zhǔn)并沒有改變 JavaScript 單線程的本質(zhì)
同步任務(wù)和異步任務(wù)
程序里面所有的任務(wù),可以分成兩類:同步任務(wù)(synchronous)和異步任務(wù)(asynchronous)。
同步任務(wù)是那些沒有被引擎掛起、在主線程上排隊(duì)執(zhí)行的任務(wù);只有前一個(gè)任務(wù)執(zhí)行完畢,才能執(zhí)行后一個(gè)任務(wù)。異步任務(wù)是那些被引擎放在一邊,不進(jìn)入主線程、而進(jìn)入任務(wù)隊(duì)列的任務(wù)。只有引擎認(rèn)為某個(gè)異步任務(wù)可以執(zhí)行了(比如 Ajax 操作從服務(wù)器得到了結(jié)果),該任務(wù)(采用回調(diào)函數(shù)的形式)才會(huì)進(jìn)入主線程執(zhí)行。排在異步任務(wù)后面的代碼,不用等待異步任務(wù)結(jié)束會(huì)馬上運(yùn)行,也就是說,異步任務(wù)不具有“堵塞”效應(yīng)。
舉例來說,Ajax 操作可以當(dāng)作同步任務(wù)處理,也可以當(dāng)作異步任務(wù)處理,由開發(fā)者決定。如果是同步任務(wù),主線程就等著 Ajax 操作返回結(jié)果,再往下執(zhí)行;如果是異步任務(wù),主線程在發(fā)出 Ajax 請(qǐng)求以后,就直接往下執(zhí)行,等到 Ajax 操作有了結(jié)果,主線程再執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù)
任務(wù)隊(duì)列和事件循環(huán)
JavaScript 運(yùn)行時(shí),除了一個(gè)正在運(yùn)行的主線程,引擎還提供一個(gè)任務(wù)隊(duì)列(task queue),里面是各種需要當(dāng)前程序處理的異步任務(wù)。(實(shí)際上,根據(jù)異步任務(wù)的類型,存在多個(gè)任務(wù)隊(duì)列。為了方便理解,這里假設(shè)只存在一個(gè)隊(duì)列)
首先,主線程會(huì)去執(zhí)行所有的同步任務(wù);等到同步任務(wù)全部執(zhí)行完,就會(huì)去看任務(wù)隊(duì)列里面的異步任務(wù);如果滿足條件,那么異步任務(wù)就重新進(jìn)入主線程開始執(zhí)行,這時(shí)它就變成同步任務(wù)了。等到執(zhí)行完,下一個(gè)異步任務(wù)再進(jìn)入主線程開始執(zhí)行。一旦任務(wù)隊(duì)列清空,程序就結(jié)束執(zhí)行。
異步任務(wù)的寫法通常是回調(diào)函數(shù)。一旦異步任務(wù)重新進(jìn)入主線程,就會(huì)執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù)。如果一個(gè)異步任務(wù)沒有回調(diào)函數(shù),就不會(huì)進(jìn)入任務(wù)隊(duì)列,也就是說,不會(huì)重新進(jìn)入主線程,因?yàn)闆]有用回調(diào)函數(shù)指定下一步的操作。
JavaScript 引擎怎么知道異步任務(wù)有沒有結(jié)果,能不能進(jìn)入主線程呢?答案就是引擎在不停地檢查,一遍又一遍,只要同步任務(wù)執(zhí)行完了,引擎就會(huì)去檢查那些掛起來的異步任務(wù),是不是可以進(jìn)入主線程了。這種循環(huán)檢查的機(jī)制,就叫做事件循環(huán)(Event Loop)。維基百科的定義是:“事件循環(huán)是一個(gè)程序結(jié)構(gòu),用于等待和發(fā)送消息和事件(a programming construct that waits for and dispatches events or messages in a program)”
異步操作的模式
下面總結(jié)一下異步操作的幾種模式
回調(diào)函數(shù)
回調(diào)函數(shù)是異步操作最基本的方法。下面是兩個(gè)函數(shù)f1和f2,編程的意圖是f2必須等到f1執(zhí)行完成,才能執(zhí)行
function f1() {// ... } function f2() {// ... } f1(); f2();上面代碼的問題在于,如果f1是異步操作,f2會(huì)立即執(zhí)行,不會(huì)等到f1結(jié)束再執(zhí)行。這時(shí),可以考慮改寫f1,把f2寫成f1的回調(diào)函數(shù)
function f1(callback) {// ...callback(); } function f2() {// ... } f1(f2);回調(diào)函數(shù)的優(yōu)點(diǎn)是簡(jiǎn)單、容易理解和實(shí)現(xiàn),缺點(diǎn)是不利于代碼的閱讀和維護(hù),各個(gè)部分之間高度耦合(coupling),使得程序結(jié)構(gòu)混亂、流程難以追蹤(尤其是多個(gè)回調(diào)函數(shù)嵌套的情況),而且每個(gè)任務(wù)只能指定一個(gè)回調(diào)函數(shù)
事件監(jiān)聽
另一種思路是采用事件驅(qū)動(dòng)模式。異步任務(wù)的執(zhí)行不取決于代碼的順序,而取決于某個(gè)事件是否發(fā)生。還是以f1和f2為例;首先,為f1綁定一個(gè)事件
f1.on('done', f2);上面這行代碼的意思是,當(dāng)f1發(fā)生done事件,就執(zhí)行f2。然后,對(duì)f1進(jìn)行改寫:
function f1() {setTimeout(function () {// ...f1.trigger('done');}, 1000); }上面代碼中,f1.trigger('done')表示,執(zhí)行完成后,立即觸發(fā)done事件,從而開始執(zhí)行f2。這種方法的優(yōu)點(diǎn)是比較容易理解,可以綁定多個(gè)事件,每個(gè)事件可以指定多個(gè)回調(diào)函數(shù),而且可以“去耦合”(decoupling),有利于實(shí)現(xiàn)模塊化。缺點(diǎn)是整個(gè)程序都要變成事件驅(qū)動(dòng)型,運(yùn)行流程會(huì)變得很不清晰。閱讀代碼的時(shí)候,很難看出主流程
發(fā)布/訂閱
事件完全可以理解成“信號(hào)”,如果存在一個(gè)“信號(hào)中心”,某個(gè)任務(wù)執(zhí)行完成,就向信號(hào)中心“發(fā)布”(publish)一個(gè)信號(hào),其他任務(wù)可以向信號(hào)中心“訂閱”(subscribe)這個(gè)信號(hào),從而知道什么時(shí)候自己可以開始執(zhí)行;這就叫做”發(fā)布/訂閱模式”(publish-subscribe pattern),又稱“觀察者模式”(observer pattern)。
這個(gè)模式有多種實(shí)現(xiàn),下面采用的是 Ben Alman 的 Tiny Pub/Sub,這是 jQuery 的一個(gè)插件。
首先,f2向信號(hào)中心jQuery訂閱done信號(hào)
jQuery.subscribe('done', f2);然后,f1進(jìn)行如下改寫
function f1() {setTimeout(function () {// ...jQuery.publish('done');}, 1000); }上面代碼中,jQuery.publish('done')的意思是,f1執(zhí)行完成后,向信號(hào)中心jQuery發(fā)布done信號(hào),從而引發(fā)f2的執(zhí)行;f2完成執(zhí)行后,可以取消訂閱(unsubscribe)
jQuery.unsubscribe('done', f2);這種方法的性質(zhì)與“事件監(jiān)聽”類似,但是明顯優(yōu)于后者。因?yàn)榭梢酝ㄟ^查看“消息中心”,了解存在多少信號(hào)、每個(gè)信號(hào)有多少訂閱者,從而監(jiān)控程序的運(yùn)行
異步操作的流程控制
如果有多個(gè)異步操作,就存在一個(gè)流程控制的問題:如何確定異步操作執(zhí)行的順序,以及如何保證遵守這種順序
function async(arg, callback) {console.log('參數(shù)為 ' + arg +' , 1秒后返回結(jié)果');setTimeout(function () { callback(arg * 2); }, 1000); }上面代碼的async函數(shù)是一個(gè)異步任務(wù),非常耗時(shí),每次執(zhí)行需要1秒才能完成,然后再調(diào)用回調(diào)函數(shù);如果有六個(gè)這樣的異步任務(wù),需要全部完成后,才能執(zhí)行最后的final函數(shù)。請(qǐng)問應(yīng)該如何安排操作流程?
function final(value) {console.log('完成: ', value); } async(1, function (value) {async(2, function (value) {async(3, function (value) {async(4, function (value) {async(5, function (value) {async(6, final);});});});}); }); // 參數(shù)為 1 , 1秒后返回結(jié)果 // 參數(shù)為 2 , 1秒后返回結(jié)果 // 參數(shù)為 3 , 1秒后返回結(jié)果 // 參數(shù)為 4 , 1秒后返回結(jié)果 // 參數(shù)為 5 , 1秒后返回結(jié)果 // 參數(shù)為 6 , 1秒后返回結(jié)果 // 完成: 12上面代碼中,六個(gè)回調(diào)函數(shù)的嵌套,不僅寫起來麻煩,容易出錯(cuò),而且難以維護(hù)
串行執(zhí)行
我們可以編寫一個(gè)流程控制函數(shù),讓它來控制異步任務(wù),一個(gè)任務(wù)完成以后,再執(zhí)行另一個(gè)。這就叫串行執(zhí)行
var items = [ 1, 2, 3, 4, 5, 6 ]; var results = []; function async(arg, callback) {console.log('參數(shù)為 ' + arg +' , 1秒后返回結(jié)果');setTimeout(function () { callback(arg * 2); }, 1000); } function final(value) {console.log('完成: ', value); } function series(item) {if(item) {async( item, function(result) {results.push(result);return series(items.shift());});} else {return final(results[results.length - 1]);} } series(items.shift());上面代碼中,函數(shù)series就是串行函數(shù),它會(huì)依次執(zhí)行異步任務(wù),所有任務(wù)都完成后,才會(huì)執(zhí)行final函數(shù)。items數(shù)組保存每一個(gè)異步任務(wù)的參數(shù),results數(shù)組保存每一個(gè)異步任務(wù)的運(yùn)行結(jié)果。注意,上面的寫法需要六秒,才能完成整個(gè)腳本
并行執(zhí)行
流程控制函數(shù)也可以是并行執(zhí)行,即所有異步任務(wù)同時(shí)執(zhí)行,等到全部完成以后,才執(zhí)行final函數(shù)
var items = [ 1, 2, 3, 4, 5, 6 ]; var results = []; function async(arg, callback) {console.log('參數(shù)為 ' + arg +' , 1秒后返回結(jié)果');setTimeout(function () { callback(arg * 2); }, 1000); } function final(value) {console.log('完成: ', value); } items.forEach(function(item) {async(item, function(result){results.push(result);if(results.length === items.length) {final(results[results.length - 1]);}}) });上面代碼中,forEach方法會(huì)同時(shí)發(fā)起六個(gè)異步任務(wù),等到它們?nèi)客瓿梢院?#xff0c;才會(huì)執(zhí)行final函數(shù)。相比而言,上面的寫法只要一秒就能完成整個(gè)腳本;這就是說,并行執(zhí)行的效率較高,比起串行執(zhí)行一次只能執(zhí)行一個(gè)任務(wù),較為節(jié)約時(shí)間;但是問題在于如果并行的任務(wù)較多,很容易耗盡系統(tǒng)資源,拖慢運(yùn)行速度。因此有了第三種流程控制方式
并行與串行的結(jié)合
所謂并行與串行的結(jié)合,就是設(shè)置一個(gè)門檻,每次最多只能并行執(zhí)行n個(gè)異步任務(wù),這樣就避免了過分占用系統(tǒng)資源
var items = [ 1, 2, 3, 4, 5, 6 ]; var results = []; var running = 0; var limit = 2; function async(arg, callback) {console.log('參數(shù)為 ' + arg +' , 1秒后返回結(jié)果');setTimeout(function () { callback(arg * 2); }, 1000); } function final(value) {console.log('完成: ', value); } function launcher() {while(running < limit && items.length > 0) {var item = items.shift();async(item, function(result) {results.push(result);running--;if(items.length > 0) {launcher();} else if(running == 0) {final(results);}});running++;} } launcher();上面代碼中,最多只能同時(shí)運(yùn)行兩個(gè)異步任務(wù)。變量running記錄當(dāng)前正在運(yùn)行的任務(wù)數(shù),只要低于門檻值,就再啟動(dòng)一個(gè)新的任務(wù),如果等于0,就表示所有任務(wù)都執(zhí)行完了,這時(shí)就執(zhí)行final函數(shù)。
這段代碼需要三秒完成整個(gè)腳本,處在串行執(zhí)行和并行執(zhí)行之間。通過調(diào)節(jié)limit變量,達(dá)到效率和資源的最佳平衡
定時(shí)器
JavaScript 提供定時(shí)執(zhí)行代碼的功能,叫做定時(shí)器(timer),主要由setTimeout()和setInterval()這兩個(gè)函數(shù)來完成。它們向任務(wù)隊(duì)列添加定時(shí)任務(wù)
setTimeout()
setTimeout函數(shù)用來指定某個(gè)函數(shù)或某段代碼,在多少毫秒之后執(zhí)行。它返回一個(gè)整數(shù),表示定時(shí)器的編號(hào),以后可以用來取消這個(gè)定時(shí)器
var timerId = setTimeout(func|code, delay);上面代碼中,setTimeout函數(shù)接受兩個(gè)參數(shù),第一個(gè)參數(shù)func|code是將要推遲執(zhí)行的函數(shù)名或者一段代碼,第二個(gè)參數(shù)delay是推遲執(zhí)行的毫秒數(shù)
console.log(1); setTimeout('console.log(2)',1000); console.log(3); // 1 // 3 // 2上面代碼會(huì)先輸出1和3,然后等待1000毫秒再輸出2。注意,console.log(2)必須以字符串的形式,作為setTimeout的參數(shù)。如果推遲執(zhí)行的是函數(shù),就直接將函數(shù)名,作為setTimeout的參數(shù)
function f() { console.log(2); } setTimeout(f, 1000);setTimeout的第二個(gè)參數(shù)如果省略,則默認(rèn)為0
setTimeout(f) // 等同于 setTimeout(f, 0)除了前兩個(gè)參數(shù),setTimeout還允許更多的參數(shù)。它們將依次傳入推遲執(zhí)行的函數(shù)(回調(diào)函數(shù))
setTimeout(function (a,b) {console.log(a + b); }, 1000, 1, 1);上面代碼中,setTimeout共有4個(gè)參數(shù)。最后兩個(gè)參數(shù),將在1000毫秒之后回調(diào)函數(shù)執(zhí)行時(shí)作為回調(diào)函數(shù)的參數(shù)。還有一個(gè)需要注意的地方,如果回調(diào)函數(shù)是對(duì)象的方法,那么setTimeout使得方法內(nèi)部的this關(guān)鍵字指向全局環(huán)境,而不是定義時(shí)所在的那個(gè)對(duì)象
var x = 1; var obj = {x: 2,y: function () { console.log(this.x); } }; setTimeout(obj.y, 1000) // 1上面代碼輸出的是1,而不是2。因?yàn)楫?dāng)obj.y在1000毫秒后運(yùn)行時(shí),this所指向的已經(jīng)不是obj了,而是全局環(huán)境。為了防止出現(xiàn)這個(gè)問題,一種解決方法是將obj.y放入一個(gè)函數(shù)
var x = 1; var obj = {x: 2,y: function () { console.log(this.x); } }; setTimeout(function () { obj.y(); }, 1000); // 2上面代碼中,obj.y放在一個(gè)匿名函數(shù)之中,這使得obj.y在obj的作用域執(zhí)行,而不是在全局作用域內(nèi)執(zhí)行,所以能夠顯示正確的值;另一種解決方法是,使用bind方法,將obj.y這個(gè)方法綁定在obj上面
var x = 1; var obj = {x: 2,y: function () { console.log(this.x); } }; setTimeout(obj.y.bind(obj), 1000) // 2setInterval()
setInterval函數(shù)的用法與setTimeout完全一致,區(qū)別僅僅在于setInterval指定某個(gè)任務(wù)每隔一段時(shí)間就執(zhí)行一次,也就是無限次的定時(shí)執(zhí)行
var i = 1 var timer = setInterval(function() { console.log(2); }, 1000)上面代碼中,每隔1000毫秒就輸出一個(gè)2,會(huì)無限運(yùn)行下去,直到關(guān)閉當(dāng)前窗口;與setTimeout一樣,除了前兩個(gè)參數(shù),setInterval方法還可以接受更多的參數(shù),它們會(huì)傳入回調(diào)函數(shù)。下面是一個(gè)通過setInterval方法實(shí)現(xiàn)網(wǎng)頁動(dòng)畫的例子
var div = document.getElementById('someDiv'); var opacity = 1; var fader = setInterval(function() {opacity -= 0.1;if (opacity >= 0) {div.style.opacity = opacity;} else {clearInterval(fader);} }, 100);上面代碼每隔100毫秒,設(shè)置一次div元素的透明度,直至其完全透明為止。setInterval的一個(gè)常見用途是實(shí)現(xiàn)輪詢。下面是一個(gè)輪詢 URL 的 Hash 值是否發(fā)生變化的例子
var hash = window.location.hash; var hashWatcher = setInterval(function() {if (window.location.hash != hash) {updatePage();} }, 1000);setInterval指定的是“開始執(zhí)行”之間的間隔,并不考慮每次任務(wù)執(zhí)行本身所消耗的時(shí)間。因此實(shí)際上,兩次執(zhí)行之間的間隔會(huì)小于指定的時(shí)間。比如,setInterval指定每 100ms 執(zhí)行一次,每次執(zhí)行需要 5ms,那么第一次執(zhí)行結(jié)束后95毫秒,第二次執(zhí)行就會(huì)開始。如果某次執(zhí)行耗時(shí)特別長(zhǎng),比如需要105毫秒,那么它結(jié)束后,下一次執(zhí)行就會(huì)立即開始。為了確保兩次執(zhí)行之間有固定的間隔,可以不用setInterval,而是每次執(zhí)行結(jié)束后,使用setTimeout指定下一次執(zhí)行的具體時(shí)間
var i = 1; var timer = setTimeout(function f() {// ...timer = setTimeout(f, 2000); }, 2000);上面代碼可以確保,下一次執(zhí)行總是在本次執(zhí)行結(jié)束之后的2000毫秒開始
clearTimeout(),clearInterval()
setTimeout和setInterval函數(shù),都返回一個(gè)整數(shù)值,表示計(jì)數(shù)器編號(hào)。將該整數(shù)傳入clearTimeout和clearInterval函數(shù),就可以取消對(duì)應(yīng)的定時(shí)器
var id1 = setTimeout(f, 1000); var id2 = setInterval(f, 1000); clearTimeout(id1); clearInterval(id2);上面代碼中,回調(diào)函數(shù)f不會(huì)再執(zhí)行了,因?yàn)閮蓚€(gè)定時(shí)器都被取消了。setTimeout和setInterval返回的整數(shù)值是連續(xù)的,也就是說,第二個(gè)setTimeout方法返回的整數(shù)值,將比第一個(gè)的整數(shù)值大1
function f() {} setTimeout(f, 1000) // 10 setTimeout(f, 1000) // 11 setTimeout(f, 1000) // 12上面代碼中,連續(xù)調(diào)用三次setTimeout,返回值都比上一次大了1。利用這一點(diǎn),可以寫一個(gè)函數(shù),取消當(dāng)前所有的setTimeout定時(shí)器
(function() { // 每輪事件循環(huán)檢查一次 var gid = setInterval(clearAllTimeouts, 0);function clearAllTimeouts() {var id = setTimeout(function() {}, 0);while (id > 0) {if (id !== gid) { clearTimeout(id); }id--;}} })();上面代碼中,先調(diào)用setTimeout,得到一個(gè)計(jì)算器編號(hào),然后把編號(hào)比它小的計(jì)數(shù)器全部取消
實(shí)例:debounce 函數(shù)
有時(shí),我們不希望回調(diào)函數(shù)被頻繁調(diào)用。比如,用戶填入網(wǎng)頁輸入框的內(nèi)容,希望通過 Ajax 方法傳回服務(wù)器,jQuery 的寫法如下:
$('textarea').on('keydown', ajaxAction);這樣寫有一個(gè)很大的缺點(diǎn),就是如果用戶連續(xù)擊鍵,就會(huì)連續(xù)觸發(fā)keydown事件,造成大量的 Ajax 通信。這是不必要的,而且很可能產(chǎn)生性能問題。正確的做法應(yīng)該是,設(shè)置一個(gè)門檻值,表示兩次 Ajax 通信的最小間隔時(shí)間。如果在間隔時(shí)間內(nèi),發(fā)生新的keydown事件,則不觸發(fā) Ajax 通信,并且重新開始計(jì)時(shí)。如果過了指定時(shí)間,沒有發(fā)生新的keydown事件,再將數(shù)據(jù)發(fā)送出去。
這種做法叫做 debounce(防抖動(dòng))。假定兩次 Ajax 通信的間隔不得小于2500毫秒,上面的代碼可以改寫成下面這樣
$('textarea').on('keydown', debounce(ajaxAction, 2500)); function debounce(fn, delay){var timer = null; // 聲明計(jì)時(shí)器return function() {var context = this;var args = arguments;clearTimeout(timer);timer = setTimeout(function () { fn.apply(context, args); }, delay);}; }上面代碼中,只要在2500毫秒之內(nèi),用戶再次擊鍵,就會(huì)取消上一次的定時(shí)器,然后再新建一個(gè)定時(shí)器。這樣就保證了回調(diào)函數(shù)之間的調(diào)用間隔,至少是2500毫秒
運(yùn)行機(jī)制
setTimeout和setInterval的運(yùn)行機(jī)制,是將指定的代碼移出本輪事件循環(huán),等到下一輪事件循環(huán),再檢查是否到了指定時(shí)間。如果到了,就執(zhí)行對(duì)應(yīng)的代碼;如果不到,就繼續(xù)等待。這意味著,setTimeout和setInterval指定的回調(diào)函數(shù),必須等到本輪事件循環(huán)的所有同步任務(wù)都執(zhí)行完,才會(huì)開始執(zhí)行。由于前面的任務(wù)到底需要多少時(shí)間執(zhí)行完,是不確定的,所以沒有辦法保證,setTimeout和setInterval指定的任務(wù),一定會(huì)按照預(yù)定時(shí)間執(zhí)行
setTimeout(someTask, 100); veryLongTask();上面代碼的setTimeout,指定100毫秒以后運(yùn)行一個(gè)任務(wù)。但是,如果后面的veryLongTask函數(shù)(同步任務(wù))運(yùn)行時(shí)間非常長(zhǎng),過了100毫秒還無法結(jié)束,那么被推遲運(yùn)行的someTask就只有等著,等到veryLongTask運(yùn)行結(jié)束,才輪到它執(zhí)行。再看一個(gè)setInterval的例子
setInterval(function () {console.log(2); }, 1000); sleep(3000); function sleep(ms) {var start = Date.now();while ((Date.now() - start) < ms) {} }setTimeout(f, 0)
含義
setTimeout的作用是將代碼推遲到指定時(shí)間執(zhí)行,如果指定時(shí)間為0,即setTimeout(f, 0),那么會(huì)立刻執(zhí)行嗎?
答案是不會(huì)。因?yàn)樯弦还?jié)說過,必須要等到當(dāng)前腳本的同步任務(wù),全部處理完以后,才會(huì)執(zhí)行setTimeout指定的回調(diào)函數(shù)f。也就是說,setTimeout(f, 0)會(huì)在下一輪事件循環(huán)一開始就執(zhí)行
setTimeout(function () { console.log(1); }, 0); console.log(2); // 2 // 1上面代碼先輸出2,再輸出1。因?yàn)?是同步任務(wù),在本輪事件循環(huán)執(zhí)行,而1是下一輪事件循環(huán)執(zhí)行。總之,setTimeout(f, 0)這種寫法的目的是,盡可能早地執(zhí)行f,但是并不能保證立刻就執(zhí)行f。實(shí)際上,setTimeout(f, 0)不會(huì)真的在0毫秒之后運(yùn)行,不同的瀏覽器有不同的實(shí)現(xiàn)。以 Edge 瀏覽器為例,會(huì)等到4毫秒之后運(yùn)行。如果電腦正在使用電池供電,會(huì)等到16毫秒之后運(yùn)行;如果網(wǎng)頁不在當(dāng)前 Tab 頁,會(huì)推遲到1000毫秒(1秒)之后運(yùn)行。這樣是為了節(jié)省系統(tǒng)資源
應(yīng)用
setTimeout(f, 0)有幾個(gè)非常重要的用途。它的一大應(yīng)用是,可以調(diào)整事件的發(fā)生順序。比如,網(wǎng)頁開發(fā)中,某個(gè)事件先發(fā)生在子元素,然后冒泡到父元素,即子元素的事件回調(diào)函數(shù),會(huì)早于父元素的事件回調(diào)函數(shù)觸發(fā)。如果,想讓父元素的事件回調(diào)函數(shù)先發(fā)生,就要用到setTimeout(f, 0)
<input type="button" id="myButton" value="click"> <script>var input = document.getElementById('myButton');input.onclick = function A() {setTimeout(function B() { input.value +=' input'; }, 0)};document.body.onclick = function C() { input.value += ' body' }; </script>上面代碼在點(diǎn)擊按鈕后,先觸發(fā)回調(diào)函數(shù)A,然后觸發(fā)函數(shù)C。函數(shù)A中,setTimeout將函數(shù)B推遲到下一輪事件循環(huán)執(zhí)行,這樣就起到了,先觸發(fā)父元素的回調(diào)函數(shù)C的目的了。
另一個(gè)應(yīng)用是,用戶自定義的回調(diào)函數(shù),通常在瀏覽器的默認(rèn)動(dòng)作之前觸發(fā)。比如,用戶在輸入框輸入文本,keypress事件會(huì)在瀏覽器接收文本之前觸發(fā)。因此,下面的回調(diào)函數(shù)是達(dá)不到目的的
<input type="text" id="input-box"> document.getElementById('input-box').onkeypress = function (event) { this.value = this.value.toUpperCase(); }上面代碼想在用戶每次輸入文本后,立即將字符轉(zhuǎn)為大寫。但是實(shí)際上,它只能將本次輸入前的字符轉(zhuǎn)為大寫,因?yàn)闉g覽器此時(shí)還沒接收到新的文本,所以this.value取不到最新輸入的那個(gè)字符。只有用setTimeout改寫,上面的代碼才能發(fā)揮作用
document.getElementById('input-box').onkeypress = function() {var self = this;setTimeout(function() { self.value = self.value.toUpperCase(); }, 0); }由于setTimeout(f, 0)實(shí)際上意味著,將任務(wù)放到瀏覽器最早可得的空閑時(shí)段執(zhí)行,所以那些計(jì)算量大、耗時(shí)長(zhǎng)的任務(wù),常常會(huì)被放到幾個(gè)小部分,分別放到setTimeout(f, 0)里面執(zhí)行
var div = document.getElementsByTagName('div')[0]; // 寫法一 for (var i = 0xA00000; i < 0xFFFFFF; i++) {div.style.backgroundColor = '#' + i.toString(16); } // 寫法二 var timer; var i=0x100000; function func() {timer = setTimeout(func, 0);div.style.backgroundColor = '#' + i.toString(16);if (i++ == 0xFFFFFF) clearTimeout(timer); } timer = setTimeout(func, 0);上面代碼有兩種寫法,都是改變一個(gè)網(wǎng)頁元素的背景色。寫法一會(huì)造成瀏覽器“堵塞”,因?yàn)?JavaScript 執(zhí)行速度遠(yuǎn)高于 DOM,會(huì)造成大量 DOM 操作“堆積”,而寫法二就不會(huì),這就是setTimeout(f, 0)的好處。
另一個(gè)使用這種技巧的例子是代碼高亮的處理。如果代碼塊很大,一次性處理,可能會(huì)對(duì)性能造成很大的壓力,那么將其分成一個(gè)個(gè)小塊,一次處理一塊,比如寫成setTimeout(highlightNext, 50)的樣子,性能壓力就會(huì)減輕。
Promise 對(duì)象
概述
Promise 對(duì)象是 JavaScript 的異步操作解決方案,為異步操作提供統(tǒng)一接口。它起到代理作用(proxy),充當(dāng)異步操作與回調(diào)函數(shù)之間的中介,使得異步操作具備同步操作的接口。Promise 可以讓異步操作寫起來,就像在寫同步操作的流程,而不必一層層地嵌套回調(diào)函數(shù)。
首先,Promise 是一個(gè)對(duì)象,也是一個(gè)構(gòu)造函數(shù)
function f1(resolve, reject) {// 異步代碼... } var p1 = new Promise(f1);上面代碼中,Promise構(gòu)造函數(shù)接受一個(gè)回調(diào)函數(shù)f1作為參數(shù),f1里面是異步操作的代碼。然后,返回的p1就是一個(gè) Promise 實(shí)例。Promise 的設(shè)計(jì)思想是,所有異步任務(wù)都返回一個(gè) Promise 實(shí)例。Promise 實(shí)例有一個(gè)then方法,用來指定下一步的回調(diào)函數(shù)
var p1 = new Promise(f1); p1.then(f2);上面代碼中,f1的異步操作執(zhí)行完成,就會(huì)執(zhí)行f2。傳統(tǒng)的寫法可能需要把f2作為回調(diào)函數(shù)傳入f1,比如寫成f1(f2),異步操作完成后,在f1內(nèi)部調(diào)用f2。Promise 使得f1和f2變成了鏈?zhǔn)綄懛ā2粌H改善了可讀性,而且對(duì)于多層嵌套的回調(diào)函數(shù)尤其方便
// 傳統(tǒng)寫法 step1(function (value1) {step2(value1, function(value2) {step3(value2, function(value3) {step4(value3, function(value4) {// ...});});}); }); // Promise 的寫法 (new Promise(step1)).then(step2).then(step3).then(step4);從上面代碼可以看到,采用 Promise 以后,程序流程變得非常清楚,十分易讀。總的來說,傳統(tǒng)的回調(diào)函數(shù)寫法使得代碼混成一團(tuán),變得橫向發(fā)展而不是向下發(fā)展。Promise 就是解決這個(gè)問題,使得異步流程可以寫成同步流程。Promise 原本只是社區(qū)提出的一個(gè)構(gòu)想,一些函數(shù)庫率先實(shí)現(xiàn)了這個(gè)功能。ECMAScript 6 將其寫入語言標(biāo)準(zhǔn),目前 JavaScript 原生支持 Promise 對(duì)象
Promise 對(duì)象的狀態(tài)
Promise 對(duì)象通過自身的狀態(tài),來控制異步操作。Promise 實(shí)例具有三種狀態(tài):異步操作未完成(pending)/異步操作成功(fulfilled)/異步操作失敗(rejected);上面三種狀態(tài)里面,fulfilled和rejected合在一起稱為resolved(已定型)。
這三種的狀態(tài)的變化途徑只有兩種:從“未完成”到“成功” / 從“未完成”到“失敗”。一旦狀態(tài)發(fā)生變化就凝固了,不會(huì)再有新的狀態(tài)變化。這也是 Promise 這個(gè)名字的由來,它的英語意思是“承諾”,一旦承諾成效,就不能再改變了。這也意味著,Promise 實(shí)例的狀態(tài)變化只可能發(fā)生一次。因此,Promise 的最終結(jié)果只有兩種:
異步操作成功,Promise 實(shí)例傳回一個(gè)值(value),狀態(tài)變?yōu)閒ulfilled
異步操作失敗,Promise 實(shí)例拋出一個(gè)錯(cuò)誤(error),狀態(tài)變?yōu)閞ejected
Promise 構(gòu)造函數(shù)
JavaScript 提供原生的Promise構(gòu)造函數(shù),用來生成 Promise 實(shí)例
var promise = new Promise(function (resolve, reject) {// ...if (/* 異步操作成功 */){resolve(value);} else { /* 異步操作失敗 */reject(new Error());} });上面代碼中,Promise構(gòu)造函數(shù)接受一個(gè)函數(shù)作為參數(shù),該函數(shù)的兩個(gè)參數(shù)分別是resolve和reject。它們是兩個(gè)函數(shù),由 JavaScript 引擎提供,不用自己實(shí)現(xiàn)。resolve函數(shù)的作用是,將Promise實(shí)例的狀態(tài)從“未完成”變?yōu)椤俺晒Α?#xff08;即從pending變?yōu)閒ulfilled),在異步操作成功時(shí)調(diào)用,并將異步操作的結(jié)果作為參數(shù)傳遞出去。reject函數(shù)的作用是,將Promise實(shí)例的狀態(tài)從“未完成”變?yōu)椤笆 ?#xff08;即從pending變?yōu)閞ejected),在異步操作失敗時(shí)調(diào)用,并將異步操作報(bào)出的錯(cuò)誤,作為參數(shù)傳遞出去
function timeout(ms) {return new Promise((resolve, reject) => { setTimeout(resolve, ms, 'done'); }); } timeout(100)上面代碼中,timeout(100)返回一個(gè) Promise 實(shí)例。100毫秒以后,該實(shí)例的狀態(tài)會(huì)變?yōu)閒ulfilled
Promise.prototype.then()
Promise 實(shí)例的then方法,用來添加回調(diào)函數(shù)。then方法可以接受兩個(gè)回調(diào)函數(shù),第一個(gè)是異步操作成功時(shí)(變?yōu)閒ulfilled狀態(tài))的回調(diào)函數(shù),第二個(gè)是異步操作失敗(變?yōu)閞ejected)時(shí)的回調(diào)函數(shù)(該參數(shù)可以省略)。一旦狀態(tài)改變,就調(diào)用相應(yīng)的回調(diào)函數(shù)
var p1 = new Promise(function (resolve, reject) { resolve('成功'); }); p1.then(console.log, console.error); // "成功" var p2 = new Promise(function (resolve, reject) { reject(new Error('失敗')); }); p2.then(console.log, console.error); // Error: 失敗上面代碼中,p1和p2都是Promise 實(shí)例,它們的then方法綁定兩個(gè)回調(diào)函數(shù):成功時(shí)的回調(diào)函數(shù)console.log,失敗時(shí)的回調(diào)函數(shù)console.error(可以省略)。p1的狀態(tài)變?yōu)槌晒?#xff0c;p2的狀態(tài)變?yōu)槭?#xff0c;對(duì)應(yīng)的回調(diào)函數(shù)會(huì)收到異步操作傳回的值,然后在控制臺(tái)輸出。then方法可以鏈?zhǔn)绞褂?/p> p1.then(step1).then(step2).then(step3).then( console.log, console.error);
上面代碼中,p1后面有四個(gè)then,意味著依次有四個(gè)回調(diào)函數(shù)。只要前一步的狀態(tài)變?yōu)閒ulfilled,就會(huì)依次執(zhí)行緊跟在后面的回調(diào)函數(shù)。最后一個(gè)then方法,回調(diào)函數(shù)是console.log和console.error,用法上有一點(diǎn)重要的區(qū)別;console.log只顯示step3的返回值,而console.error可以顯示p1、step1、step2、step3之中任意一個(gè)發(fā)生的錯(cuò)誤。舉例來說,如果step1的狀態(tài)變?yōu)閞ejected,那么step2和step3都不會(huì)執(zhí)行了(因?yàn)樗鼈兪莚esolved的回調(diào)函數(shù))。Promise 開始尋找,接下來第一個(gè)為rejected的回調(diào)函數(shù),在上面代碼中是console.error。這就是說,Promise 對(duì)象的報(bào)錯(cuò)具有傳遞性
then() 用法辨析
Promise 的用法,簡(jiǎn)單說就是一句話:使用then方法添加回調(diào)函數(shù)。但是,不同的寫法有一些細(xì)微的差別,請(qǐng)看下面四種寫法:
// 寫法一 f1().then(function () { return f2(); }); // 寫法二 f1().then(function () { f2(); }); // 寫法三 f1().then(f2()); // 寫法四 f1().then(f2);為了便于講解,下面這四種寫法都再用then方法接一個(gè)回調(diào)函數(shù)f3。寫法一的f3回調(diào)函數(shù)的參數(shù),是f2函數(shù)的運(yùn)行結(jié)果
f1().then(function () { return f2(); }).then(f3);寫法二的f3回調(diào)函數(shù)的參數(shù)是undefined
f1().then(function () { f2(); return; }).then(f3);寫法三的f3回調(diào)函數(shù)的參數(shù),是f2函數(shù)返回的函數(shù)的運(yùn)行結(jié)果
f1().then(f2()).then(f3);寫法四與寫法一只有一個(gè)差別,那就是f2會(huì)接收到f1()返回的結(jié)果
f1().then(f2).then(f3);實(shí)例:圖片加載
下面是使用 Promise 完成圖片的加載
var preloadImage = function (path) {return new Promise(function (resolve, reject) {var image = new Image();image.onload = resolve;image.onerror = reject;image.src = path;}); };上面的preloadImage函數(shù)用法如下
preloadImage('https://example.com/my.jpg').then(function (e) { document.body.append(e.target) }).then(function () { console.log('加載成功') })小結(jié)
Promise 的優(yōu)點(diǎn)在于,讓回調(diào)函數(shù)變成了規(guī)范的鏈?zhǔn)綄懛?#xff0c;程序流程可以看得很清楚。它有一整套接口,可以實(shí)現(xiàn)許多強(qiáng)大的功能,比如同時(shí)執(zhí)行多個(gè)異步操作,等到它們的狀態(tài)都改變以后,再執(zhí)行一個(gè)回調(diào)函數(shù);再比如,為多個(gè)回調(diào)函數(shù)中拋出的錯(cuò)誤,統(tǒng)一指定處理方法等等。而且,Promise 還有一個(gè)傳統(tǒng)寫法沒有的好處:它的狀態(tài)一旦改變,無論何時(shí)查詢,都能得到這個(gè)狀態(tài)。這意味著,無論何時(shí)為 Promise 實(shí)例添加回調(diào)函數(shù),該函數(shù)都能正確執(zhí)行。所以,你不用擔(dān)心是否錯(cuò)過了某個(gè)事件或信號(hào)。如果是傳統(tǒng)寫法,通過監(jiān)聽事件來執(zhí)行回調(diào)函數(shù),一旦錯(cuò)過了事件,再添加回調(diào)函數(shù)是不會(huì)執(zhí)行的。
Promise 的缺點(diǎn)是,編寫的難度比傳統(tǒng)寫法高,而且閱讀代碼也不是一眼可以看懂。你只會(huì)看到一堆then,必須自己在then的回調(diào)函數(shù)里面理清邏輯
微任務(wù)
Promise 的回調(diào)函數(shù)屬于異步任務(wù),會(huì)在同步任務(wù)之后執(zhí)行
new Promise(function (resolve, reject) { resolve(1); }).then(console.log); console.log(2); // 2 // 1上面代碼會(huì)先輸出2,再輸出1。因?yàn)閏onsole.log(2)是同步任務(wù),而then的回調(diào)函數(shù)屬于異步任務(wù),一定晚于同步任務(wù)執(zhí)行;但是,Promise 的回調(diào)函數(shù)不是正常的異步任務(wù),而是微任務(wù)(microtask)。它們的區(qū)別在于,正常任務(wù)追加到下一輪事件循環(huán),微任務(wù)追加到本輪事件循環(huán)。這意味著,微任務(wù)的執(zhí)行時(shí)間一定早于正常任務(wù)
setTimeout(function() { console.log(1); }, 0); new Promise(function (resolve, reject) { resolve(2); }).then(console.log); console.log(3); // 3 // 2 // 1上面代碼的輸出結(jié)果是321。這說明then的回調(diào)函數(shù)的執(zhí)行時(shí)間,早于setTimeout(fn, 0)。因?yàn)閠hen是本輪事件循環(huán)執(zhí)行,setTimeout(fn, 0)在下一輪事件循環(huán)開始時(shí)執(zhí)行
DOM
DOM 概述
DOM
DOM 是 JavaScript 操作網(wǎng)頁的接口,全稱為“文檔對(duì)象模型”(Document Object Model)。它的作用是將網(wǎng)頁轉(zhuǎn)為一個(gè) JavaScript 對(duì)象,從而可以用腳本進(jìn)行各種操作(比如增刪內(nèi)容)。瀏覽器會(huì)根據(jù) DOM 模型,將結(jié)構(gòu)化文檔(比如 HTML 和 XML)解析成一系列的節(jié)點(diǎn),再由這些節(jié)點(diǎn)組成一個(gè)樹狀結(jié)構(gòu)(DOM Tree)。所有的節(jié)點(diǎn)和最終的樹狀結(jié)構(gòu),都有規(guī)范的對(duì)外接口
DOM 只是一個(gè)接口規(guī)范,可以用各種語言實(shí)現(xiàn)。所以嚴(yán)格地說,DOM 不是 JavaScript 語法的一部分,但是 DOM 操作是 JavaScript 最常見的任務(wù),離開了 DOM,JavaScript 就無法控制網(wǎng)頁。另一方面,JavaScript 也是最常用于 DOM 操作的語言
節(jié)點(diǎn)
DOM 的最小組成單位叫做節(jié)點(diǎn)(node)。文檔的樹形結(jié)構(gòu)(DOM 樹),就是由各種不同類型的節(jié)點(diǎn)組成。每個(gè)節(jié)點(diǎn)可以看作是文檔樹的一片葉子
節(jié)點(diǎn)的類型有七種。
1.Document:整個(gè)文檔樹的頂層節(jié)點(diǎn)
2.DocumentType:doctype標(biāo)簽(比如)
3.Element:網(wǎng)頁的各種HTML標(biāo)簽(比如
、等)4.Attribute:網(wǎng)頁元素的屬性(比如class="right")
5.Text:標(biāo)簽之間或標(biāo)簽包含的文本
6.Comment:注釋
7.DocumentFragment:文檔的片段
瀏覽器提供一個(gè)原生的節(jié)點(diǎn)對(duì)象Node,上面這七種節(jié)點(diǎn)都繼承了Node,因此具有一些共同的屬性和方法
節(jié)點(diǎn)樹
一個(gè)文檔的所有節(jié)點(diǎn),按照所在的層級(jí),可以抽象成一種樹狀結(jié)構(gòu);這種樹狀結(jié)構(gòu)就是 DOM 樹。它有一個(gè)頂層節(jié)點(diǎn),下一層都是頂層節(jié)點(diǎn)的子節(jié)點(diǎn),然后子節(jié)點(diǎn)又有自己的子節(jié)點(diǎn),就這樣層層衍生出一個(gè)金字塔結(jié)構(gòu),倒過來就像一棵樹。瀏覽器原生提供document節(jié)點(diǎn),代表整個(gè)文檔
document // 整個(gè)文檔樹文檔的第一層只有一個(gè)節(jié)點(diǎn),就是 HTML 網(wǎng)頁的第一個(gè)標(biāo)簽,它構(gòu)成了樹結(jié)構(gòu)的根節(jié)點(diǎn)(root node),其他 HTML 標(biāo)簽節(jié)點(diǎn)都是它的下級(jí)節(jié)點(diǎn);除了根節(jié)點(diǎn),其他節(jié)點(diǎn)都有三種層級(jí)關(guān)系:
1.父節(jié)點(diǎn)關(guān)系(parentNode):直接的那個(gè)上級(jí)節(jié)點(diǎn)
2.子節(jié)點(diǎn)關(guān)系(childNodes):直接的下級(jí)節(jié)點(diǎn)
3.同級(jí)節(jié)點(diǎn)關(guān)系(sibling):擁有同一個(gè)父節(jié)點(diǎn)的節(jié)點(diǎn)
DOM 提供操作接口,用來獲取這三種關(guān)系的節(jié)點(diǎn)。比如,子節(jié)點(diǎn)接口包括firstChild(第一個(gè)子節(jié)點(diǎn))和lastChild(最后一個(gè)子節(jié)點(diǎn))等屬性,同級(jí)節(jié)點(diǎn)接口包括nextSibling(緊鄰在后的那個(gè)同級(jí)節(jié)點(diǎn))和previousSibling(緊鄰在前的那個(gè)同級(jí)節(jié)點(diǎn))屬性
Node 接口
所有 DOM 節(jié)點(diǎn)對(duì)象都繼承了 Node 接口,擁有一些共同的屬性和方法。這是 DOM 操作的基礎(chǔ)
屬性
Node.prototype.nodeType
nodeType屬性返回一個(gè)整數(shù)值,表示節(jié)點(diǎn)的類型
document.nodeType // 9上面代碼中,文檔節(jié)點(diǎn)的類型值為9。Node 對(duì)象定義了幾個(gè)常量,對(duì)應(yīng)這些類型值
document.nodeType === Node.DOCUMENT_NODE // true上面代碼中,文檔節(jié)點(diǎn)的nodeType屬性等于常量Node.DOCUMENT_NODE;不同節(jié)點(diǎn)的nodeType屬性值和對(duì)應(yīng)的常量如下:
1.文檔節(jié)點(diǎn)(document):9,對(duì)應(yīng)常量Node.DOCUMENT_NODE
2.元素節(jié)點(diǎn)(element):1,對(duì)應(yīng)常量Node.ELEMENT_NODE
3.屬性節(jié)點(diǎn)(attr):2,對(duì)應(yīng)常量Node.ATTRIBUTE_NODE
4.文本節(jié)點(diǎn)(text):3,對(duì)應(yīng)常量Node.TEXT_NODE
5.文檔片斷節(jié)點(diǎn)(DocumentFragment):11,對(duì)應(yīng)常量Node.DOCUMENT_FRAGMENT_NODE
6.文檔類型節(jié)點(diǎn)(DocumentType):10,對(duì)應(yīng)常量Node.DOCUMENT_TYPE_NODE
7.注釋節(jié)點(diǎn)(Comment):8,對(duì)應(yīng)常量Node.COMMENT_NODE
確定節(jié)點(diǎn)類型時(shí),使用nodeType屬性是常用方法
var node = document.documentElement.firstChild; if (node.nodeType === Node.ELEMENT_NODE) { console.log('該節(jié)點(diǎn)是元素節(jié)點(diǎn)'); }Node.prototype.nodeName
nodeName屬性返回節(jié)點(diǎn)的名稱
<div id="d1">hello world</div> <script>var div = document.getElementById('d1');div.nodeName // "DIV" </script>上面代碼中,元素節(jié)點(diǎn)
的nodeName屬性就是大寫的標(biāo)簽名DIV;不同節(jié)點(diǎn)的nodeName屬性值如下:1.文檔節(jié)點(diǎn)(document):#document
2.元素節(jié)點(diǎn)(element):大寫的標(biāo)簽名
3.屬性節(jié)點(diǎn)(attr):屬性的名稱
4.文本節(jié)點(diǎn)(text):#text
5.文檔片斷節(jié)點(diǎn)(DocumentFragment):#document-fragment
6.文檔類型節(jié)點(diǎn)(DocumentType):文檔的類型
7.注釋節(jié)點(diǎn)(Comment):#comment
Node.prototype.nodeValue
nodeValue屬性返回一個(gè)字符串,表示當(dāng)前節(jié)點(diǎn)本身的文本值,該屬性可讀寫;只有文本節(jié)點(diǎn)(text)、注釋節(jié)點(diǎn)(comment)和屬性節(jié)點(diǎn)(attr)有文本值,因此這三類節(jié)點(diǎn)的nodeValue可以返回結(jié)果,其他類型的節(jié)點(diǎn)一律返回null。同樣的,也只有這三類節(jié)點(diǎn)可以設(shè)置nodeValue屬性的值,其他類型的節(jié)點(diǎn)設(shè)置無效
<div id="d1">hello world</div> <script>var div = document.getElementById('d1');div.nodeValue // nulldiv.firstChild.nodeValue // "hello world" </script>上面代碼中,div是元素節(jié)點(diǎn),nodeValue屬性返回null。div.firstChild是文本節(jié)點(diǎn),所以可以返回文本值
Node.prototype.textContent
textContent屬性返回當(dāng)前節(jié)點(diǎn)和它的所有后代節(jié)點(diǎn)的文本內(nèi)容
<div id="divA">This is <span>some</span> text</div> document.getElementById('divA').textContent // This is some texttextContent屬性自動(dòng)忽略當(dāng)前節(jié)點(diǎn)內(nèi)部的 HTML 標(biāo)簽,返回所有文本內(nèi)容;該屬性是可讀寫的,設(shè)置該屬性的值,會(huì)用一個(gè)新的文本節(jié)點(diǎn),替換所有原來的子節(jié)點(diǎn)。它還有一個(gè)好處,就是自動(dòng)對(duì) HTML 標(biāo)簽轉(zhuǎn)義。這很適合用于用戶提供的內(nèi)容
document.getElementById('foo').textContent = '<p>GoodBye!</p>'上面代碼在插入文本時(shí),會(huì)將
標(biāo)簽解釋為文本,而不會(huì)當(dāng)作標(biāo)簽處理。
對(duì)于文本節(jié)點(diǎn)(text)、注釋節(jié)點(diǎn)(comment)和屬性節(jié)點(diǎn)(attr),textContent屬性的值與nodeValue屬性相同。對(duì)于其他類型的節(jié)點(diǎn),該屬性會(huì)將每個(gè)子節(jié)點(diǎn)(不包括注釋節(jié)點(diǎn))的內(nèi)容連接在一起返回。如果一個(gè)節(jié)點(diǎn)沒有子節(jié)點(diǎn),則返回空字符串。文檔節(jié)點(diǎn)(document)和文檔類型節(jié)點(diǎn)(doctype)的textContent屬性為null。如果要讀取整個(gè)文檔的內(nèi)容,可以使用document.documentElement.textContent
Node.prototype.baseURI
baseURI屬性返回一個(gè)字符串,表示當(dāng)前網(wǎng)頁的絕對(duì)路徑。瀏覽器根據(jù)這個(gè)屬性,計(jì)算網(wǎng)頁上的相對(duì)路徑的 URL。該屬性為只讀
document.baseURI如果無法讀到網(wǎng)頁的 URL,baseURI屬性返回null;該屬性的值一般由當(dāng)前網(wǎng)址的 URL(即window.location屬性)決定,但是可以使用 HTML 的標(biāo)簽,改變?cè)搶傩缘闹?/p> <base href="http://www.example.com/page.html">
設(shè)置了以后,baseURI屬性就返回標(biāo)簽設(shè)置的值
Node.prototype.ownerDocument
Node.ownerDocument屬性返回當(dāng)前節(jié)點(diǎn)所在的頂層文檔對(duì)象,即document對(duì)象
var d = p.ownerDocument; d === document // truedocument對(duì)象本身的ownerDocument屬性,返回null
Node.prototype.nextSibling
Node.nextSibling屬性返回緊跟在當(dāng)前節(jié)點(diǎn)后面的第一個(gè)同級(jí)節(jié)點(diǎn);如果當(dāng)前節(jié)點(diǎn)后面沒有同級(jí)節(jié)點(diǎn),則返回null
<div id="d1">hello</div><div id="d2">world</div> var d1 = document.getElementById('d1'); var d2 = document.getElementById('d2'); d1.nextSibling === d2 // true注意,該屬性還包括文本節(jié)點(diǎn)和注釋節(jié)點(diǎn)()。因此如果當(dāng)前節(jié)點(diǎn)后面有空格,該屬性會(huì)返回一個(gè)文本節(jié)點(diǎn),內(nèi)容為空格;nextSibling屬性可以用來遍歷所有子節(jié)點(diǎn)
var el = document.getElementById('div1').firstChild; while (el !== null) {console.log(el.nodeName);el = el.nextSibling; }上面代碼遍歷div1節(jié)點(diǎn)的所有子節(jié)點(diǎn)
Node.prototype.previousSibling
previousSibling屬性返回當(dāng)前節(jié)點(diǎn)前面的、距離最近的一個(gè)同級(jí)節(jié)點(diǎn)。如果當(dāng)前節(jié)點(diǎn)前面沒有同級(jí)節(jié)點(diǎn),則返回null
<div id="d1">hello</div><div id="d2">world</div> var d1 = document.getElementById('d1'); var d2 = document.getElementById('d2'); d2.previousSibling === d1 // true上面代碼中,d2.previousSibling就是d2前面的同級(jí)節(jié)點(diǎn)d1。注意,該屬性還包括文本節(jié)點(diǎn)和注釋節(jié)點(diǎn)。因此如果當(dāng)前節(jié)點(diǎn)前面有空格,該屬性會(huì)返回一個(gè)文本節(jié)點(diǎn),內(nèi)容為空格
Node.prototype.parentNode
parentNode屬性返回當(dāng)前節(jié)點(diǎn)的父節(jié)點(diǎn)。對(duì)于一個(gè)節(jié)點(diǎn)來說,它的父節(jié)點(diǎn)只可能是三種類型:元素節(jié)點(diǎn)(element)、文檔節(jié)點(diǎn)(document)和文檔片段節(jié)點(diǎn)(documentfragment)
if (node.parentNode) { node.parentNode.removeChild(node); }上面代碼中,通過node.parentNode屬性將node節(jié)點(diǎn)從文檔里面移除;文檔節(jié)點(diǎn)(document)和文檔片段節(jié)點(diǎn)(documentfragment)的父節(jié)點(diǎn)都是null。另外,對(duì)于那些生成后還沒插入 DOM 樹的節(jié)點(diǎn),父節(jié)點(diǎn)也是null
Node.prototype.parentElement
parentElement屬性返回當(dāng)前節(jié)點(diǎn)的父元素節(jié)點(diǎn);如果當(dāng)前節(jié)點(diǎn)沒有父節(jié)點(diǎn),或者父節(jié)點(diǎn)類型不是元素節(jié)點(diǎn),則返回null
if (node.parentElement) { node.parentElement.style.color = 'red'; }上面代碼中,父元素節(jié)點(diǎn)的樣式設(shè)定了紅色;由于父節(jié)點(diǎn)只可能是三種類型:元素節(jié)點(diǎn)、文檔節(jié)點(diǎn)(document)和文檔片段節(jié)點(diǎn)(documentfragment);parentElement屬性相當(dāng)于把后兩種父節(jié)點(diǎn)都排除了
Node.prototype.firstChild,Node.prototype.lastChild
firstChild屬性返回當(dāng)前節(jié)點(diǎn)的第一個(gè)子節(jié)點(diǎn),如果當(dāng)前節(jié)點(diǎn)沒有子節(jié)點(diǎn),則返回null
<p id="p1"><span>First span</span></p> var p1 = document.getElementById('p1'); p1.firstChild.nodeName // "SPAN"注意,firstChild返回的除了元素節(jié)點(diǎn),還可能是文本節(jié)點(diǎn)或注釋節(jié)點(diǎn)
<p id="p1"><span>First span</span></p> var p1 = document.getElementById('p1'); p1.firstChild.nodeName // "#text"lastChild屬性返回當(dāng)前節(jié)點(diǎn)的最后一個(gè)子節(jié)點(diǎn),如果當(dāng)前節(jié)點(diǎn)沒有子節(jié)點(diǎn),則返回null。用法與firstChild屬性相同
Node.prototype.childNodes
childNodes屬性返回一個(gè)類似數(shù)組的對(duì)象(NodeList集合),成員包括當(dāng)前節(jié)點(diǎn)的所有子節(jié)點(diǎn)
var children = document.querySelector('ul').childNodes;上面代碼中,children就是ul元素的所有子節(jié)點(diǎn);使用該屬性,可以遍歷某個(gè)節(jié)點(diǎn)的所有子節(jié)點(diǎn)
var div = document.getElementById('div1'); var children = div.childNodes; for (var i = 0; i < children.length; i++) {// ... }文檔節(jié)點(diǎn)(document)就有兩個(gè)子節(jié)點(diǎn):文檔類型節(jié)點(diǎn)(docType)和 HTML 根元素節(jié)點(diǎn)
var children = document.childNodes; for (var i = 0; i < children.length; i++) { console.log(children[i].nodeType); } // 10 // 1上面代碼中,文檔節(jié)點(diǎn)的第一個(gè)子節(jié)點(diǎn)的類型是10(即文檔類型節(jié)點(diǎn)),第二個(gè)子節(jié)點(diǎn)的類型是1(即元素節(jié)點(diǎn))
注意,除了元素節(jié)點(diǎn),childNodes屬性的返回值還包括文本節(jié)點(diǎn)和注釋節(jié)點(diǎn);如果當(dāng)前節(jié)點(diǎn)不包括任何子節(jié)點(diǎn),則返回一個(gè)空的NodeList集合。由于NodeList對(duì)象是一個(gè)動(dòng)態(tài)集合,一旦子節(jié)點(diǎn)發(fā)生變化,立刻會(huì)反映在返回結(jié)果之中
Node.prototype.isConnected
isConnected屬性返回一個(gè)布爾值,表示當(dāng)前節(jié)點(diǎn)是否在文檔之中
var test = document.createElement('p'); test.isConnected // false document.body.appendChild(test); test.isConnected // true上面代碼中,test節(jié)點(diǎn)是腳本生成的節(jié)點(diǎn),沒有插入文檔之前,isConnected屬性返回false,插入之后返回true
方法
Node.prototype.appendChild()
appendChild()方法接受一個(gè)節(jié)點(diǎn)對(duì)象作為參數(shù),將其作為最后一個(gè)子節(jié)點(diǎn),插入當(dāng)前節(jié)點(diǎn)。該方法的返回值就是插入文檔的子節(jié)點(diǎn)
var p = document.createElement('p'); document.body.appendChild(p);上面代碼新建一個(gè)
節(jié)點(diǎn),將其插入document.body的尾部;如果參數(shù)節(jié)點(diǎn)是 DOM 已經(jīng)存在的節(jié)點(diǎn),appendChild()方法會(huì)將其從原來的位置,移動(dòng)到新位置
var div = document.getElementById('myDiv'); document.body.appendChild(div);上面代碼中,插入的是一個(gè)已經(jīng)存在的節(jié)點(diǎn)myDiv,結(jié)果就是該節(jié)點(diǎn)會(huì)從原來的位置,移動(dòng)到document.body的尾部;如果appendChild()方法的參數(shù)是DocumentFragment節(jié)點(diǎn),那么插入的是DocumentFragment的所有子節(jié)點(diǎn),而不是DocumentFragment節(jié)點(diǎn)本身。返回值是一個(gè)空的DocumentFragment節(jié)點(diǎn)
Node.prototype.hasChildNodes()
hasChildNodes方法返回一個(gè)布爾值,表示當(dāng)前節(jié)點(diǎn)是否有子節(jié)點(diǎn)
var foo = document.getElementById('foo'); if (foo.hasChildNodes()) { foo.removeChild(foo.childNodes[0]); }上面代碼表示,如果foo節(jié)點(diǎn)有子節(jié)點(diǎn),就移除第一個(gè)子節(jié)點(diǎn);注意,子節(jié)點(diǎn)包括所有類型的節(jié)點(diǎn),并不僅僅是元素節(jié)點(diǎn)。哪怕節(jié)點(diǎn)只包含一個(gè)空格,hasChildNodes方法也會(huì)返回true。判斷一個(gè)節(jié)點(diǎn)有沒有子節(jié)點(diǎn),有許多種方法,下面是其中的三種:
1.node.hasChildNodes()
2.node.firstChild !== null
3.node.childNodes && node.childNodes.length > 0
hasChildNodes方法結(jié)合firstChild屬性和nextSibling屬性,可以遍歷當(dāng)前節(jié)點(diǎn)的所有后代節(jié)點(diǎn)
function DOMComb(parent, callback) {if (parent.hasChildNodes()) {for (var node = parent.firstChild; node; node = node.nextSibling) { DOMComb(node, callback); }}callback(parent); } DOMComb(document.body, console.log) // 用法上面代碼中,DOMComb函數(shù)的第一個(gè)參數(shù)是某個(gè)指定的節(jié)點(diǎn),第二個(gè)參數(shù)是回調(diào)函數(shù)。這個(gè)回調(diào)函數(shù)會(huì)依次作用于指定節(jié)點(diǎn),以及指定節(jié)點(diǎn)的所有后代節(jié)點(diǎn)
Node.prototype.cloneNode()
cloneNode方法用于克隆一個(gè)節(jié)點(diǎn)。它接受一個(gè)布爾值作為參數(shù),表示是否同時(shí)克隆子節(jié)點(diǎn)。它的返回值是一個(gè)克隆出來的新節(jié)點(diǎn)
var cloneUL = document.querySelector('ul').cloneNode(true);該方法有一些使用注意點(diǎn):
1.克隆一個(gè)節(jié)點(diǎn),會(huì)拷貝該節(jié)點(diǎn)的所有屬性,但是會(huì)喪失addEventListener方法和on-屬性(即node.onclick = fn),添加在這個(gè)節(jié)點(diǎn)上的事件回調(diào)函數(shù)
2.該方法返回的節(jié)點(diǎn)不在文檔之中,即沒有任何父節(jié)點(diǎn),必須使用諸如Node.appendChild這樣的方法添加到文檔之中。
3.克隆一個(gè)節(jié)點(diǎn)之后,DOM 有可能出現(xiàn)兩個(gè)有相同id屬性(即id="xxx")的網(wǎng)頁元素,這時(shí)應(yīng)該修改其中一個(gè)元素的id屬性。如果原節(jié)點(diǎn)有name屬性,可能也需要修改
Node.prototype.insertBefore()
insertBefore方法用于將某個(gè)節(jié)點(diǎn)插入父節(jié)點(diǎn)內(nèi)部的指定位置
var insertedNode = parentNode.insertBefore(newNode, referenceNode);insertBefore方法接受兩個(gè)參數(shù),第一個(gè)參數(shù)是所要插入的節(jié)點(diǎn)newNode,第二個(gè)參數(shù)是父節(jié)點(diǎn)parentNode內(nèi)部的一個(gè)子節(jié)點(diǎn)referenceNode。newNode將插在referenceNode這個(gè)子節(jié)點(diǎn)的前面。返回值是插入的新節(jié)點(diǎn)newNode
var p = document.createElement('p'); document.body.insertBefore(p, document.body.firstChild);上面代碼中,新建一個(gè)
節(jié)點(diǎn),插在document.body.firstChild的前面,也就是成為document.body的第一個(gè)子節(jié)點(diǎn)。如果insertBefore方法的第二個(gè)參數(shù)為null,則新節(jié)點(diǎn)將插在當(dāng)前節(jié)點(diǎn)內(nèi)部的最后位置,即變成最后一個(gè)子節(jié)點(diǎn)
var p = document.createElement('p'); document.body.insertBefore(p, null);上面代碼中,p將成為document.body的最后一個(gè)子節(jié)點(diǎn)。這也說明insertBefore的第二個(gè)參數(shù)不能省略。
注意,如果所要插入的節(jié)點(diǎn)是當(dāng)前 DOM 現(xiàn)有的節(jié)點(diǎn),則該節(jié)點(diǎn)將從原有的位置移除,插入新的位置。
由于不存在insertAfter方法,如果新節(jié)點(diǎn)要插在父節(jié)點(diǎn)的某個(gè)子節(jié)點(diǎn)后面,可以用insertBefore方法結(jié)合nextSibling屬性模擬
parent.insertBefore(s1, s2.nextSibling);上面代碼中,parent是父節(jié)點(diǎn),s1是一個(gè)全新的節(jié)點(diǎn),s2是可以將s1節(jié)點(diǎn),插在s2節(jié)點(diǎn)的后面。如果s2是當(dāng)前節(jié)點(diǎn)的最后一個(gè)子節(jié)點(diǎn),則s2.nextSibling返回null,這時(shí)s1節(jié)點(diǎn)會(huì)插在當(dāng)前節(jié)點(diǎn)的最后,變成當(dāng)前節(jié)點(diǎn)的最后一個(gè)子節(jié)點(diǎn),等于緊跟在s2的后面。如果要插入的節(jié)點(diǎn)是DocumentFragment類型,那么插入的將是DocumentFragment的所有子節(jié)點(diǎn),而不是DocumentFragment節(jié)點(diǎn)本身。返回值將是一個(gè)空的DocumentFragment節(jié)點(diǎn)
Node.prototype.removeChild()
removeChild方法接受一個(gè)子節(jié)點(diǎn)作為參數(shù),用于從當(dāng)前節(jié)點(diǎn)移除該子節(jié)點(diǎn)。返回值是移除的子節(jié)點(diǎn)
var divA = document.getElementById('A'); divA.parentNode.removeChild(divA);上面代碼移除了divA節(jié)點(diǎn)。注意,這個(gè)方法是在divA的父節(jié)點(diǎn)上調(diào)用的,不是在divA上調(diào)用的。下面是如何移除當(dāng)前節(jié)點(diǎn)的所有子節(jié)點(diǎn)
var element = document.getElementById('top'); while (element.firstChild) { element.removeChild(element.firstChild); }被移除的節(jié)點(diǎn)依然存在于內(nèi)存之中,但不再是 DOM 的一部分。所以,一個(gè)節(jié)點(diǎn)移除以后,依然可以使用它,比如插入到另一個(gè)節(jié)點(diǎn)下面。如果參數(shù)節(jié)點(diǎn)不是當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn),removeChild方法將報(bào)錯(cuò)
Node.prototype.replaceChild()
replaceChild方法用于將一個(gè)新的節(jié)點(diǎn),替換當(dāng)前節(jié)點(diǎn)的某一個(gè)子節(jié)點(diǎn)
var replacedNode = parentNode.replaceChild(newChild, oldChild);上面代碼中,replaceChild方法接受兩個(gè)參數(shù),第一個(gè)參數(shù)newChild是用來替換的新節(jié)點(diǎn),第二個(gè)參數(shù)oldChild是將要替換走的子節(jié)點(diǎn)。返回值是替換走的那個(gè)節(jié)點(diǎn)oldChild
var divA = document.getElementById('divA'); var newSpan = document.createElement('span'); newSpan.textContent = 'Hello World!'; divA.parentNode.replaceChild(newSpan, divA);Node.prototype.contains()
contains方法返回一個(gè)布爾值,表示參數(shù)節(jié)點(diǎn)是否滿足以下三個(gè)條件之一:
1.參數(shù)節(jié)點(diǎn)為當(dāng)前節(jié)點(diǎn)
2.參數(shù)節(jié)點(diǎn)為當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn)
3.參數(shù)節(jié)點(diǎn)為當(dāng)前節(jié)點(diǎn)的后代節(jié)點(diǎn)
document.body.contains(node)上面代碼檢查參數(shù)節(jié)點(diǎn)node,是否包含在當(dāng)前文檔之中
注意,當(dāng)前節(jié)點(diǎn)傳入contains方法,返回true
nodeA.contains(nodeA) // trueNode.prototype.compareDocumentPosition()
compareDocumentPosition方法的用法,與contains方法完全一致,返回一個(gè)六個(gè)比特位的二進(jìn)制值,表示參數(shù)節(jié)點(diǎn)與當(dāng)前節(jié)點(diǎn)的關(guān)系
二進(jìn)制值十進(jìn)制值含義0000000兩個(gè)節(jié)點(diǎn)相同0000011兩個(gè)節(jié)點(diǎn)不在同一個(gè)文檔(即有一個(gè)節(jié)點(diǎn)不在當(dāng)前文檔)0000102參數(shù)節(jié)點(diǎn)在當(dāng)前節(jié)點(diǎn)的前面0001004參數(shù)節(jié)點(diǎn)在當(dāng)前節(jié)點(diǎn)的后面0010008參數(shù)節(jié)點(diǎn)包含當(dāng)前節(jié)點(diǎn)01000016當(dāng)前節(jié)點(diǎn)包含參數(shù)節(jié)點(diǎn)10000032瀏覽器內(nèi)部使用<div id="mydiv"><form><input id="test" /></form> </div> <script> var div = document.getElementById('mydiv'); var input = document.getElementById('test'); div.compareDocumentPosition(input) // 20 input.compareDocumentPosition(div) // 10 </script>上面代碼中,節(jié)點(diǎn)div包含節(jié)點(diǎn)input(二進(jìn)制010000),而且節(jié)點(diǎn)input在節(jié)點(diǎn)div的后面(二進(jìn)制000100),所以第一個(gè)compareDocumentPosition方法返回20(二進(jìn)制010100,即010000 + 000100),第二個(gè)compareDocumentPosition方法返回10(二進(jìn)制001010)。由于compareDocumentPosition返回值的含義,定義在每一個(gè)比特位上,所以如果要檢查某一種特定的含義,就需要使用比特位運(yùn)算符
var head = document.head; var body = document.body; if (head.compareDocumentPosition(body) & 4) {console.log('文檔結(jié)構(gòu)正確'); } else {console.log('<body> 不能在 <head> 前面'); }上面代碼中,compareDocumentPosition的返回值與4(又稱掩碼)進(jìn)行與運(yùn)算(&),得到一個(gè)布爾值,表示
是否在前面Node.prototype.isEqualNode(),Node.prototype.isSameNode()
isEqualNode方法返回一個(gè)布爾值,用于檢查兩個(gè)節(jié)點(diǎn)是否相等。所謂相等的節(jié)點(diǎn),指的是兩個(gè)節(jié)點(diǎn)的類型相同、屬性相同、子節(jié)點(diǎn)相同
var p1 = document.createElement('p'); var p2 = document.createElement('p'); p1.isEqualNode(p2) // trueisSameNode方法返回一個(gè)布爾值,表示兩個(gè)節(jié)點(diǎn)是否為同一個(gè)節(jié)點(diǎn)
var p1 = document.createElement('p'); var p2 = document.createElement('p'); p1.isSameNode(p2) // false p1.isSameNode(p1) // trueNode.prototype.normalize()
normalize方法用于清理當(dāng)前節(jié)點(diǎn)內(nèi)部的所有文本節(jié)點(diǎn)(text)。它會(huì)去除空的文本節(jié)點(diǎn),并且將毗鄰的文本節(jié)點(diǎn)合并成一個(gè),也就是說不存在空的文本節(jié)點(diǎn),以及毗鄰的文本節(jié)點(diǎn)
var wrapper = document.createElement('div'); wrapper.appendChild(document.createTextNode('Part 1 ')); wrapper.appendChild(document.createTextNode('Part 2 ')); wrapper.childNodes.length // 2 wrapper.normalize(); wrapper.childNodes.length // 1上面代碼使用normalize方法之前,wrapper節(jié)點(diǎn)有兩個(gè)毗鄰的文本子節(jié)點(diǎn)。使用normalize方法之后,兩個(gè)文本子節(jié)點(diǎn)被合并成一個(gè);該方法是Text.splitText的逆方法
Node.prototype.getRootNode()
getRootNode()方法返回當(dāng)前節(jié)點(diǎn)所在文檔的根節(jié)點(diǎn)document,與ownerDocument屬性的作用相同
document.body.firstChild.getRootNode() === document // true document.body.firstChild.getRootNode() === document.body.firstChild.ownerDocument // true該方法可用于document節(jié)點(diǎn)自身,這一點(diǎn)與document.ownerDocument不同
document.getRootNode() // document document.ownerDocument // nullNodeList 接口,HTMLCollection 接口
節(jié)點(diǎn)都是單個(gè)對(duì)象,有時(shí)需要一種數(shù)據(jù)結(jié)構(gòu),能夠容納多個(gè)節(jié)點(diǎn)。DOM 提供兩種節(jié)點(diǎn)集合,用于容納多個(gè)節(jié)點(diǎn):NodeList和HTMLCollection。這兩種集合都屬于接口規(guī)范;許多 DOM 屬性和方法,返回的結(jié)果是NodeList實(shí)例或HTMLCollection實(shí)例。主要區(qū)別是,NodeList可以包含各種類型的節(jié)點(diǎn),HTMLCollection只能包含 HTML 元素節(jié)點(diǎn)
NodeList 接口
概述
NodeList實(shí)例是一個(gè)類似數(shù)組的對(duì)象,它的成員是節(jié)點(diǎn)對(duì)象。通過以下方法可以得到NodeList實(shí)例:
1.Node.childNodes
2.document.querySelectorAll()等節(jié)點(diǎn)搜索方法
document.body.childNodes instanceof NodeList // trueNodeList實(shí)例很像數(shù)組,可以使用length屬性和forEach方法。但是,它不是數(shù)組,不能使用pop或push之類數(shù)組特有的方法
var children = document.body.childNodes; Array.isArray(children) // false children.length // 34 children.forEach(console.log)上面代碼中,NodeList 實(shí)例children不是數(shù)組,但是具有l(wèi)ength屬性和forEach方法。如果NodeList實(shí)例要使用數(shù)組方法,可以將其轉(zhuǎn)為真正的數(shù)組
var children = document.body.childNodes; var nodeArr = Array.prototype.slice.call(children);除了使用forEach方法遍歷 NodeList 實(shí)例,還可以使用for循環(huán)
var children = document.body.childNodes; for (var i = 0; i < children.length; i++) { var item = children[i]; }注意,NodeList 實(shí)例可能是動(dòng)態(tài)集合,也可能是靜態(tài)集合。所謂動(dòng)態(tài)集合就是一個(gè)活的集合,DOM 刪除或新增一個(gè)相關(guān)節(jié)點(diǎn),都會(huì)立刻反映在 NodeList 實(shí)例。目前,只有Node.childNodes返回的是一個(gè)動(dòng)態(tài)集合,其他的 NodeList 都是靜態(tài)集合
var children = document.body.childNodes; children.length // 18 document.body.appendChild(document.createElement('p')); children.length // 19上面代碼中,文檔增加一個(gè)子節(jié)點(diǎn),NodeList 實(shí)例children的length屬性就增加了1
NodeList.prototype.length
length屬性返回 NodeList 實(shí)例包含的節(jié)點(diǎn)數(shù)量
document.querySelectorAll('xxx').length // 0上面代碼中,document.querySelectorAll返回一個(gè) NodeList 集合。對(duì)于那些不存在的 HTML 標(biāo)簽,length屬性返回0
NodeList.prototype.forEach()
forEach方法用于遍歷 NodeList 的所有成員。它接受一個(gè)回調(diào)函數(shù)作為參數(shù),每一輪遍歷就執(zhí)行一次這個(gè)回調(diào)函數(shù),用法與數(shù)組實(shí)例的forEach方法完全一致
var children = document.body.childNodes; children.forEach(function f(item, i, list) {// ... }, this);上面代碼中,回調(diào)函數(shù)f的三個(gè)參數(shù)依次是當(dāng)前成員、位置和當(dāng)前 NodeList 實(shí)例。forEach方法的第二個(gè)參數(shù),用于綁定回調(diào)函數(shù)內(nèi)部的this,該參數(shù)可省略
NodeList.prototype.item()
item方法接受一個(gè)整數(shù)值作為參數(shù),表示成員的位置,返回該位置上的成員
document.body.childNodes.item(0)上面代碼中,item(0)返回第一個(gè)成員;如果參數(shù)值大于實(shí)際長(zhǎng)度,或者索引不合法(比如負(fù)數(shù)),item方法返回null;如果省略參數(shù),item方法會(huì)報(bào)錯(cuò);所有類似數(shù)組的對(duì)象,都可以使用方括號(hào)運(yùn)算符取出成員;一般情況下,都是使用方括號(hào)運(yùn)算符,而不使用item方法
document.body.childNodes[0]NodeList.prototype.keys(),NodeList.prototype.values(),NodeList.prototype.entries()
這三個(gè)方法都返回一個(gè) ES6 的遍歷器對(duì)象,可以通過for...of循環(huán)遍歷獲取每一個(gè)成員的信息。區(qū)別在于,keys()返回鍵名的遍歷器,values()返回鍵值的遍歷器,entries()返回的遍歷器同時(shí)包含鍵名和鍵值的信息
var children = document.body.childNodes; for (var key of children.keys()) { console.log(key); } // 0 // 1 // 2 // ... for (var value of children.values()) { console.log(value); } // #text // <script> // ... for (var entry of children.entries()) { console.log(entry); } // Array [ 0, #text ] // Array [ 1, <script> ] // ...HTMLCollection 接口
概述
HTMLCollection是一個(gè)節(jié)點(diǎn)對(duì)象的集合,只能包含元素節(jié)點(diǎn)(element),不能包含其他類型的節(jié)點(diǎn)。它的返回值是一個(gè)類似數(shù)組的對(duì)象,但是與NodeList接口不同,HTMLCollection沒有forEach方法,只能使用for循環(huán)遍歷。返回HTMLCollection實(shí)例的,主要是一些Document對(duì)象的集合屬性,比如document.links、document.forms、document.images等
document.links instanceof HTMLCollection // trueHTMLCollection實(shí)例都是動(dòng)態(tài)集合,節(jié)點(diǎn)的變化會(huì)實(shí)時(shí)反映在集合中;如果元素節(jié)點(diǎn)有id或name屬性,那么HTMLCollection實(shí)例上面,可以使用id屬性或name屬性引用該節(jié)點(diǎn)元素。如果沒有對(duì)應(yīng)的節(jié)點(diǎn),則返回null
<img id="pic" src="http://example.com/foo.jpg"> var pic = document.getElementById('pic'); document.images.pic === pic // true上面代碼中,document.images是一個(gè)HTMLCollection實(shí)例,可以通過元素的id屬性值,從HTMLCollection實(shí)例上取到這個(gè)元素
HTMLCollection.prototype.length
length屬性返回HTMLCollection實(shí)例包含的成員數(shù)量
document.links.length // 18HTMLCollection.prototype.item()
item方法接受一個(gè)整數(shù)值作為參數(shù),表示成員的位置,返回該位置上的成員
var c = document.images; var img0 = c.item(0);上面代碼中,item(0)表示返回0號(hào)位置的成員。由于方括號(hào)運(yùn)算符也具有同樣作用,而且使用更方便,所以一般情況下,總是使用方括號(hào)運(yùn)算符;如果參數(shù)值超出成員數(shù)量或者不合法(比如小于0),那么item方法返回null
HTMLCollection.prototype.namedItem()
namedItem方法的參數(shù)是一個(gè)字符串,表示id屬性或name屬性的值,返回對(duì)應(yīng)的元素節(jié)點(diǎn)。如果沒有對(duì)應(yīng)的節(jié)點(diǎn),則返回null
<img id="pic" src="http://example.com/foo.jpg"> var pic = document.getElementById('pic'); document.images.namedItem('pic') === pic // trueParentNode 接口,ChildNode 接口
節(jié)點(diǎn)對(duì)象除了繼承 Node 接口以外,還會(huì)繼承其他接口。ParentNode接口表示當(dāng)前節(jié)點(diǎn)是一個(gè)父節(jié)點(diǎn),提供一些處理子節(jié)點(diǎn)的方法;ChildNode接口表示當(dāng)前節(jié)點(diǎn)是一個(gè)子節(jié)點(diǎn),提供一些相關(guān)方法
ParentNode 接口
如果當(dāng)前節(jié)點(diǎn)是父節(jié)點(diǎn),就會(huì)繼承ParentNode接口。由于只有元素節(jié)點(diǎn)(element)、文檔節(jié)點(diǎn)(document)和文檔片段節(jié)點(diǎn)(documentFragment)擁有子節(jié)點(diǎn),因此只有這三類節(jié)點(diǎn)會(huì)繼承ParentNode接口
ParentNode.children
children屬性返回一個(gè)HTMLCollection實(shí)例,成員是當(dāng)前節(jié)點(diǎn)的所有元素子節(jié)點(diǎn),該屬性只讀;下面是遍歷某個(gè)節(jié)點(diǎn)的所有元素子節(jié)點(diǎn)的示例
for (var i = 0; i < el.children.length; i++) {// ... }注意,children屬性只包括元素子節(jié)點(diǎn),不包括其他類型的子節(jié)點(diǎn)(比如文本子節(jié)點(diǎn));如果沒有元素類型的子節(jié)點(diǎn),返回值HTMLCollection實(shí)例的length屬性為0。另外,HTMLCollection是動(dòng)態(tài)集合,會(huì)實(shí)時(shí)反映 DOM 的任何變化
ParentNode.firstElementChild
firstElementChild屬性返回當(dāng)前節(jié)點(diǎn)的第一個(gè)元素子節(jié)點(diǎn);如果沒有任何元素子節(jié)點(diǎn),則返回null
document.firstElementChild.nodeName // "HTML"上面代碼中,document節(jié)點(diǎn)的第一個(gè)元素子節(jié)點(diǎn)是
ParentNode.lastElementChild
lastElementChild屬性返回當(dāng)前節(jié)點(diǎn)的最后一個(gè)元素子節(jié)點(diǎn),如果不存在任何元素子節(jié)點(diǎn),則返回null
document.lastElementChild.nodeName // "HTML"上面代碼中,document節(jié)點(diǎn)的最后一個(gè)元素子節(jié)點(diǎn)是(因?yàn)閐ocument只包含這一個(gè)元素子節(jié)點(diǎn))
ParentNode.childElementCount
childElementCount屬性返回一個(gè)整數(shù),表示當(dāng)前節(jié)點(diǎn)的所有元素子節(jié)點(diǎn)的數(shù)目。如果不包含任何元素子節(jié)點(diǎn),則返回0
document.body.childElementCount // 13ParentNode.append(),ParentNode.prepend()
append方法為當(dāng)前節(jié)點(diǎn)追加一個(gè)或多個(gè)子節(jié)點(diǎn),位置是最后一個(gè)元素子節(jié)點(diǎn)的后面;該方法不僅可以添加元素子節(jié)點(diǎn),還可以添加文本子節(jié)點(diǎn)
var parent = document.body; // 添加元素子節(jié)點(diǎn) var p = document.createElement('p'); parent.append(p); // 添加文本子節(jié)點(diǎn) parent.append('Hello'); // 添加多個(gè)元素子節(jié)點(diǎn) var p1 = document.createElement('p'); var p2 = document.createElement('p'); parent.append(p1, p2); // 添加元素子節(jié)點(diǎn)和文本子節(jié)點(diǎn) var p = document.createElement('p'); parent.append('Hello', p);注意,該方法沒有返回值;prepend方法為當(dāng)前節(jié)點(diǎn)追加一個(gè)或多個(gè)子節(jié)點(diǎn),位置是第一個(gè)元素子節(jié)點(diǎn)的前面;它的用法與append方法完全一致,也是沒有返回值
ChildNode 接口
如果一個(gè)節(jié)點(diǎn)有父節(jié)點(diǎn),那么該節(jié)點(diǎn)就繼承了ChildNode接口
ChildNode.remove()
remove方法用于從父節(jié)點(diǎn)移除當(dāng)前節(jié)點(diǎn)
el.remove()上面代碼在 DOM 里面移除了el節(jié)點(diǎn)
ChildNode.before(),ChildNode.after()
before方法用于在當(dāng)前節(jié)點(diǎn)的前面,插入一個(gè)或多個(gè)同級(jí)節(jié)點(diǎn),兩者擁有相同的父節(jié)點(diǎn);注意,該方法不僅可以插入元素節(jié)點(diǎn),還可以插入文本節(jié)點(diǎn)
var p = document.createElement('p'); var p1 = document.createElement('p'); el.before(p); // 插入元素節(jié)點(diǎn) el.before('Hello'); // 插入文本節(jié)點(diǎn) el.before(p, p1); // 插入多個(gè)元素節(jié)點(diǎn) el.before(p, 'Hello'); // 插入元素節(jié)點(diǎn)和文本節(jié)點(diǎn)after方法用于在當(dāng)前節(jié)點(diǎn)的后面,插入一個(gè)或多個(gè)同級(jí)節(jié)點(diǎn),兩者擁有相同的父節(jié)點(diǎn)。用法與before方法完全相同
ChildNode.replaceWith()
replaceWith方法使用參數(shù)節(jié)點(diǎn),替換當(dāng)前節(jié)點(diǎn)。參數(shù)可以是元素節(jié)點(diǎn),也可以是文本節(jié)點(diǎn)
var span = document.createElement('span'); el.replaceWith(span);上面代碼中,el節(jié)點(diǎn)將被span節(jié)點(diǎn)替換
Document 節(jié)點(diǎn)
概述
document節(jié)點(diǎn)對(duì)象代表整個(gè)文檔,每張網(wǎng)頁都有自己的document對(duì)象,window.document屬性就指向這個(gè)對(duì)象;只要瀏覽器開始載入 HTML 文檔,該對(duì)象就存在了,可以直接使用。document對(duì)象有不同的辦法可以獲取:
1.正常的網(wǎng)頁,直接使用document或window.document
2.iframe框架里面的網(wǎng)頁,使用iframe節(jié)點(diǎn)的contentDocument屬性
3.Ajax 操作返回的文檔,使用XMLHttpRequest對(duì)象的responseXML屬性
4.內(nèi)部節(jié)點(diǎn)的ownerDocument屬性
document對(duì)象繼承了EventTarget接口、Node接口、ParentNode接口;這意味著這些接口的方法都可以在document對(duì)象上調(diào)用;除此之外,document對(duì)象還有很多自己的屬性和方法
屬性
快捷方式屬性
以下屬性是指向文檔內(nèi)部的某個(gè)節(jié)點(diǎn)的快捷方式
document.defaultView
document.defaultView屬性返回document對(duì)象所屬的window對(duì)象。如果當(dāng)前文檔不屬于window對(duì)象,該屬性返回null
document.defaultView === window // truedocument.doctype
對(duì)于 HTML 文檔來說,document對(duì)象一般有兩個(gè)子節(jié)點(diǎn);第一個(gè)子節(jié)點(diǎn)是document.doctype,指向節(jié)點(diǎn),即文檔類型(Document Type Declaration,簡(jiǎn)寫DTD)節(jié)點(diǎn);HTML 的文檔類型節(jié)點(diǎn)一般寫成,如果網(wǎng)頁沒有聲明 DTD,該屬性返回null
var doctype = document.doctype; doctype // "<!DOCTYPE html>" doctype.name // "html"document.firstChild通常就返回這個(gè)節(jié)點(diǎn)
document.documentElement
document.documentElement屬性返回當(dāng)前文檔的根元素節(jié)點(diǎn)(root);它通常是document節(jié)點(diǎn)的第二個(gè)子節(jié)點(diǎn),緊跟在document.doctype節(jié)點(diǎn)后面;HTML網(wǎng)頁的該屬性,一般是節(jié)點(diǎn)
document.body,document.head
document.body屬性指向
節(jié)點(diǎn),document.head屬性指向節(jié)點(diǎn);這兩個(gè)屬性總是存在的,如果網(wǎng)頁源碼里面省略了或,瀏覽器會(huì)自動(dòng)創(chuàng)建;另外,這兩個(gè)屬性是可寫的,如果改寫它們的值,相當(dāng)于移除所有子節(jié)點(diǎn)document.scrollingElement
document.scrollingElement屬性返回文檔的滾動(dòng)元素。也就是說,當(dāng)文檔整體滾動(dòng)時(shí),到底是哪個(gè)元素在滾動(dòng);標(biāo)準(zhǔn)模式下,這個(gè)屬性返回的文檔的根元素document.documentElement(即)。兼容(quirk)模式下,返回的是
元素,如果該元素不存在,返回nulldocument.scrollingElement.scrollTop = 0; // 頁面滾動(dòng)到瀏覽器頂部document.activeElement
document.activeElement屬性返回獲得當(dāng)前焦點(diǎn)(focus)的 DOM 元素。通常,這個(gè)屬性返回的是、、等表單元素;如果當(dāng)前沒有焦點(diǎn)元素,返回元素或null
document.fullscreenElement
document.fullscreenElement屬性返回當(dāng)前以全屏狀態(tài)展示的 DOM 元素。如果不是全屏狀態(tài),該屬性返回null
if (document.fullscreenElement.nodeName == 'VIDEO') { console.log('全屏播放視頻'); }上面代碼中,通過document.fullscreenElement可以知道
元素有沒有處在全屏狀態(tài),從而判斷用戶行為節(jié)點(diǎn)集合屬性
以下屬性返回一個(gè)HTMLCollection實(shí)例,表示文檔內(nèi)部特定元素的集合;這些集合都是動(dòng)態(tài)的,原節(jié)點(diǎn)有任何變化,立刻會(huì)反映在集合中
document.links
document.links屬性返回當(dāng)前文檔所有設(shè)定了href屬性的
// 打印文檔所有的鏈接 var links = document.links; for(var i = 0; i < links.length; i++) {console.log(links[i]); }document.forms
document.forms屬性返回所有
表單節(jié)點(diǎn)var selectForm = document.forms[0];上面代碼獲取文檔第一個(gè)表單;除了使用位置序號(hào),id屬性和name屬性也可以用來引用表單
<form name="foo" id="bar"></form> document.forms[0] === document.forms.foo // true document.forms.bar === document.forms.foo // truedocument.images
document.images屬性返回頁面所有圖片節(jié)點(diǎn)
var imglist = document.images; for(var i = 0; i < imglist.length; i++) {if (imglist[i].src === 'banner.gif') {// ...} }上面代碼在所有img標(biāo)簽中,尋找某張圖片
document.embeds,document.plugins
document.embeds屬性和document.plugins屬性,都返回所有節(jié)點(diǎn)
document.scripts
document.scripts屬性返回所有
var scripts = document.scripts; if (scripts.length !== 0 ) { console.log('當(dāng)前網(wǎng)頁有腳本'); }document.styleSheets
document.styleSheets屬性返回文檔內(nèi)嵌或引入的樣式表集合
小結(jié)
除了document.styleSheets,以上的集合屬性返回的都是HTMLCollection實(shí)例
document.links instanceof HTMLCollection // true document.images instanceof HTMLCollection // true document.forms instanceof HTMLCollection // true document.embeds instanceof HTMLCollection // true document.scripts instanceof HTMLCollection // trueHTMLCollection實(shí)例是類似數(shù)組的對(duì)象,所以這些屬性都有l(wèi)ength屬性,都可以使用方括號(hào)運(yùn)算符引用成員。如果成員有id或name屬性,還可以用這兩個(gè)屬性的值,在HTMLCollection實(shí)例上引用到這個(gè)成員
<form name="myForm"> document.myForm === document.forms.myForm // true文檔靜態(tài)信息屬性
以下屬性返回文檔信息
document.documentURI,document.URL
document.documentURI屬性和document.URL屬性都返回一個(gè)字符串,表示當(dāng)前文檔的網(wǎng)址;不同之處是它們繼承自不同的接口,documentURI繼承自Document接口,可用于所有文檔;URL繼承自HTMLDocument接口,只能用于 HTML 文檔
document.URL // http://www.example.com/about document.documentURI === document.URL // true如果文檔的錨點(diǎn)(#anchor)變化,這兩個(gè)屬性都會(huì)跟著變化
document.domain
document.domain屬性返回當(dāng)前文檔的域名,不包含協(xié)議和接口;比如,網(wǎng)頁的網(wǎng)址是,那么domain屬性就等于www.example.com。如果無法獲取域名,該屬性返回null;document.domain基本上是一個(gè)只讀屬性,只有一種情況除外;次級(jí)域名的網(wǎng)頁,可以把document.domain設(shè)為對(duì)應(yīng)的上級(jí)域名。比如,當(dāng)前域名是a.sub.example.com,則document.domain屬性可以設(shè)置為sub.example.com,也可以設(shè)為example.com。修改后,document.domain相同的兩個(gè)網(wǎng)頁,可以讀取對(duì)方的資源,比如設(shè)置的 Cookie。
另外,設(shè)置document.domain會(huì)導(dǎo)致端口被改成null。因此,如果通過設(shè)置document.domain來進(jìn)行通信,雙方網(wǎng)頁都必須設(shè)置這個(gè)值,才能保證端口相同
document.location
Location對(duì)象是瀏覽器提供的原生對(duì)象,提供 URL 相關(guān)的信息和操作方法。通過window.location和document.location屬性,可以拿到這個(gè)對(duì)象
document.lastModified
document.lastModified屬性返回一個(gè)字符串,表示當(dāng)前文檔最后修改的時(shí)間。不同瀏覽器的返回值,日期格式是不一樣的
document.lastModified // "04/10/2019 12:17:01"注意:document.lastModified屬性的值是字符串,所以不能直接用來比較;Date.parse方法將其轉(zhuǎn)為Date實(shí)例,才能比較兩個(gè)網(wǎng)頁
var lastVisitedDate = Date.parse('01/01/2018'); if (Date.parse(document.lastModified) > lastVisitedDate) { console.log('網(wǎng)頁已經(jīng)變更'); }document.characterSet
document.characterSet屬性返回當(dāng)前文檔的編碼,比如UTF-8、ISO-8859-1等
document.referrer
document.referrer屬性返回一個(gè)字符串,表示當(dāng)前文檔的訪問者來自哪里
document.referrer // "https://example.com/path"如果無法獲取來源,或者用戶直接鍵入網(wǎng)址而不是從其他網(wǎng)頁點(diǎn)擊進(jìn)入,document.referrer返回一個(gè)空字符串;document.referrer的值,總是與 HTTP 頭信息的Referer字段保持一致。但是,document.referrer的拼寫有兩個(gè)r,而頭信息的Referer字段只有一個(gè)r
document.dir
document.dir返回一個(gè)字符串,表示文字方向。它只有兩個(gè)可能的值:rtl表示文字從右到左,阿拉伯文是這種方式;ltr表示文字從左到右,包括英語和漢語在內(nèi)的大多數(shù)文字采用這種方式
document.compatMode
compatMode屬性返回瀏覽器處理文檔的模式,可能的值為BackCompat(向后兼容模式)和CSS1Compat(嚴(yán)格模式)。一般來說,如果網(wǎng)頁代碼的第一行設(shè)置了明確的DOCTYPE(比如),document.compatMode的值都為CSS1Compat
文檔狀態(tài)屬性
document.hidden
document.hidden屬性返回一個(gè)布爾值,表示當(dāng)前頁面是否可見;如果窗口最小化、瀏覽器切換了 Tab,都會(huì)導(dǎo)致頁面不可見,使得document.hidden返回true。這個(gè)屬性是 Page Visibility API 引入的,一般都是配合這個(gè) API 使用
document.visibilityState
document.visibilityState返回文檔的可見狀態(tài);它的值有四種可能:
1.visible:頁面可見。注意,頁面可能是部分可見,即不是焦點(diǎn)窗口,前面被其他窗口部分擋住了
2.hidden:頁面不可見,有可能窗口最小化,或者瀏覽器切換到了另一個(gè) Tab
3.prerender:頁面處于正在渲染狀態(tài),對(duì)于用戶來說,該頁面不可見
4.unloaded:頁面從內(nèi)存里面卸載了
這個(gè)屬性可以用在頁面加載時(shí),防止加載某些資源;或者頁面不可見時(shí)停掉一些頁面功能
document.readyState
document.readyState屬性返回當(dāng)前文檔的狀態(tài),共有三種可能的值:
1.loading:加載 HTML 代碼階段(尚未完成解析)
2.interactive:加載外部資源階段
3.complete:加載完成
這個(gè)屬性變化的過程如下:
1.瀏覽器開始解析 HTML 文檔,document.readyState屬性等于loading
2.瀏覽器遇到 HTML 文檔中的
3.HTML 文檔解析完成,document.readyState屬性變成interactive
4.瀏覽器等待圖片、樣式表、字體文件等外部資源加載完成;一旦全部加載完成,document.readyState屬性變成complete
下面的代碼用來檢查網(wǎng)頁是否加載成功
if (document.readyState === 'complete') { // 基本檢查// ... } var interval = setInterval(function() { // 輪詢檢查if (document.readyState === 'complete') {clearInterval(interval);// ...} }, 100);另外,每次狀態(tài)變化都會(huì)觸發(fā)一個(gè)readystatechange事件
document.cookie
document.cookie屬性用來操作瀏覽器 Cookie
document.designMode
document.designMode屬性控制當(dāng)前文檔是否可編輯。該屬性只有兩個(gè)值on和off,默認(rèn)值為off。一旦設(shè)為on,用戶就可以編輯整個(gè)文檔的內(nèi)容;
document.implementation
document.implementation屬性返回一個(gè)DOMImplementation對(duì)象。該對(duì)象有三個(gè)方法,主要用于創(chuàng)建獨(dú)立于當(dāng)前文檔的新的 Document 對(duì)象;
1.DOMImplementation.createDocument():創(chuàng)建一個(gè) XML 文檔
2.DOMImplementation.createHTMLDocument():創(chuàng)建一個(gè) HTML 文檔
3.DOMImplementation.createDocumentType():創(chuàng)建一個(gè) DocumentType 對(duì)象
下面是創(chuàng)建 HTML 文檔的例子
var doc = document.implementation.createHTMLDocument('Title'); var p = doc.createElement('p'); p.innerHTML = 'hello world'; doc.body.appendChild(p); document.replaceChild(doc.documentElement,document.documentElement );上面代碼中,第一步生成一個(gè)新的 HTML 文檔doc,然后用它的根元素document.documentElement替換掉document.documentElement;這會(huì)使得當(dāng)前文檔的內(nèi)容全部消失,變成hello world
方法
document.open(),document.close()
document.open方法清除當(dāng)前文檔所有內(nèi)容,使得文檔處于可寫狀態(tài),供document.write方法寫入內(nèi)容;document.close方法用來關(guān)閉document.open()打開的文檔
document.open(); document.write('hello world'); document.close();document.write(),document.writeln()
document.write方法用于向當(dāng)前文檔寫入內(nèi)容;在網(wǎng)頁的首次渲染階段,只要頁面沒有關(guān)閉寫入(即沒有執(zhí)行document.close()),document.write寫入的內(nèi)容就會(huì)追加在已有內(nèi)容的后面
// 頁面顯示“helloworld” document.open(); document.write('hello'); document.write('world'); document.close();注意,document.write會(huì)當(dāng)作 HTML 代碼解析,不會(huì)轉(zhuǎn)義
document.write('<p>hello world</p>');上面代碼中,document.write會(huì)將
當(dāng)作 HTML 標(biāo)簽解釋;如果頁面已經(jīng)解析完成(DOMContentLoaded事件發(fā)生之后),再調(diào)用write方法,它會(huì)先調(diào)用open方法,擦除當(dāng)前文檔所有內(nèi)容,然后再寫入
document.addEventListener('DOMContentLoaded', function (event) { document.write('<p>Hello World!</p>'); }); // 等同于 document.addEventListener('DOMContentLoaded', function (event) {document.open();document.write('<p>Hello World!</p>');document.close(); });如果在頁面渲染過程中調(diào)用write方法,并不會(huì)自動(dòng)調(diào)用open方法。(可以理解成,open方法已調(diào)用,但close方法還未調(diào)用。)
<html><body> hello <script type="text/javascript">document.write("world") </script> </body></html>在瀏覽器打開上面網(wǎng)頁,將會(huì)顯示hello world;document.write是 JavaScript 語言標(biāo)準(zhǔn)化之前就存在的方法,現(xiàn)在完全有更符合標(biāo)準(zhǔn)的方法向文檔寫入內(nèi)容(比如對(duì)innerHTML屬性賦值);所以,除了某些特殊情況,應(yīng)該盡量避免使用document.write這個(gè)方法;document.writeln方法與write方法完全一致,除了會(huì)在輸出內(nèi)容的尾部添加換行符
document.write(1); document.write(2); // 12 document.writeln(1); document.writeln(2); // 1 // 2注意,writeln方法添加的是 ASCII 碼的換行符,渲染成 HTML 網(wǎng)頁時(shí)不起作用,即在網(wǎng)頁上顯示不出換行。網(wǎng)頁上的換行,必須顯式寫入
document.querySelector(),document.querySelectorAll()
document.querySelector方法接受一個(gè) CSS 選擇器作為參數(shù),返回匹配該選擇器的元素節(jié)點(diǎn);如果有多個(gè)節(jié)點(diǎn)滿足匹配條件,則返回第一個(gè)匹配的節(jié)點(diǎn);如果沒有發(fā)現(xiàn)匹配的節(jié)點(diǎn),則返回null
var el1 = document.querySelector('.myclass'); var el2 = document.querySelector('#myParent > [ng-click]');document.querySelectorAll方法與querySelector用法類似,區(qū)別是返回一個(gè)NodeList對(duì)象,包含所有匹配給定選擇器的節(jié)點(diǎn)
elementList = document.querySelectorAll('.myclass');這兩個(gè)方法的參數(shù),可以是逗號(hào)分隔的多個(gè) CSS 選擇器,返回匹配其中一個(gè)選擇器的元素節(jié)點(diǎn),這與 CSS 選擇器的規(guī)則是一致的
var matches = document.querySelectorAll('div.note, div.alert');上面代碼返回class屬性是note或alert的div元素;這兩個(gè)方法都支持復(fù)雜的 CSS 選擇器
document.querySelectorAll('[data-foo-bar="someval"]'); // 選中 data-foo-bar 屬性等于 someval 的元素 document.querySelectorAll('#myForm :invalid'); // 選中 myForm 表單中所有不通過驗(yàn)證的元素 document.querySelectorAll('DIV:not(.ignore)'); // 選中div元素,那些 class 含 ignore 的除外 document.querySelectorAll('DIV, A, SCRIPT'); // 同時(shí)選中 div,a,script 三類元素但是,它們不支持 CSS 偽元素的選擇器(比如:first-line和:first-letter)和偽類的選擇器(比如:link和:visited),即無法選中偽元素和偽類;如果querySelectorAll方法的參數(shù)是字符串*,則會(huì)返回文檔中的所有元素節(jié)點(diǎn);另外,querySelectorAll的返回結(jié)果不是動(dòng)態(tài)集合,不會(huì)實(shí)時(shí)反映元素節(jié)點(diǎn)的變化;最后,這兩個(gè)方法除了定義在document對(duì)象上,還定義在元素節(jié)點(diǎn)上,即在元素節(jié)點(diǎn)上也可以調(diào)用
document.getElementsByTagName()
document.getElementsByTagName方法搜索 HTML 標(biāo)簽名,返回符合條件的元素。它的返回值是一個(gè)類似數(shù)組對(duì)象(HTMLCollection實(shí)例),可以實(shí)時(shí)反映 HTML 文檔的變化。如果沒有任何匹配的元素,就返回一個(gè)空集
var paras = document.getElementsByTagName('p'); paras instanceof HTMLCollection // true上面代碼返回當(dāng)前文檔的所有p元素節(jié)點(diǎn);HTML 標(biāo)簽名是大小寫不敏感的,因此getElementsByTagName方法也是大小寫不敏感的。另外,返回結(jié)果中,各個(gè)成員的順序就是它們?cè)谖臋n中出現(xiàn)的順序;如果傳入*,就可以返回文檔中所有 HTML 元素
var allElements = document.getElementsByTagName('*');注意,元素節(jié)點(diǎn)本身也定義了getElementsByTagName方法,返回該元素的后代元素中符合條件的元素。也就是說,這個(gè)方法不僅可以在document對(duì)象上調(diào)用,也可以在任何元素節(jié)點(diǎn)上調(diào)用
var firstPara = document.getElementsByTagName('p')[0]; var spans = firstPara.getElementsByTagName('span');上面代碼選中第一個(gè)p元素內(nèi)部的所有span元素
document.getElementsByClassName()
document.getElementsByClassName方法返回一個(gè)類似數(shù)組的對(duì)象(HTMLCollection實(shí)例),包括了所有class名字符合指定條件的元素,元素的變化實(shí)時(shí)反映在返回結(jié)果中
var elements = document.getElementsByClassName(names);由于class是保留字,所以 JavaScript 一律使用className表示 CSS 的class;參數(shù)可以是多個(gè)class,它們之間使用空格分隔
var elements = document.getElementsByClassName('foo bar');上面代碼返回同時(shí)具有foo和bar兩個(gè)class的元素,foo和bar的順序不重要。;注意,正常模式下,CSS 的class是大小寫敏感的;(quirks mode下,大小寫不敏感。)
與getElementsByTagName方法一樣,getElementsByClassName方法不僅可以在document對(duì)象上調(diào)用,也可以在任何元素節(jié)點(diǎn)上調(diào)用
// 非document對(duì)象上調(diào)用 var elements = rootElement.getElementsByClassName(names);document.getElementsByName()
document.getElementsByName方法用于選擇擁有name屬性的 HTML 元素(比如
、、、、和總結(jié)
以上是生活随笔為你收集整理的JavaScript 教程(二)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: nb-iot简介【转】
- 下一篇: SpringBoot入门(1)——创建s