3atv精品不卡视频,97人人超碰国产精品最新,中文字幕av一区二区三区人妻少妇,久久久精品波多野结衣,日韩一区二区三区精品

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > javascript >内容正文

javascript

JavaScript 教程(二)

發布時間:2025/6/17 javascript 16 豆豆
生活随笔 收集整理的這篇文章主要介紹了 JavaScript 教程(二) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

面向對象編程

實例對象與 new 命令

JavaScript 語言具有很強的面向對象編程能力,這里介紹 JavaScript 面向對象編程的基礎知識

對象是什么

面向對象編程(Object Oriented Programming,縮寫為 OOP)是目前主流的編程范式。它將真實世界各種復雜的關系,抽象為一個個對象,然后由對象之間的分工與合作,完成對真實世界的模擬。每一個對象都是功能中心,具有明確分工,可以完成接受信息、處理數據、發出信息等任務。對象可以復用,通過繼承機制還可以定制。因此,面向對象編程具有靈活、代碼可復用、高度模塊化等特點,容易維護和開發,比起由一系列函數或指令組成的傳統的過程式編程(procedural programming),更適合多人合作的大型軟件項目。那么,“對象”(object)到底是什么?我們從兩個層次來理解

對象是單個實物的抽象

一本書、一輛汽車、一個人都可以是對象,一個數據庫、一張網頁、一個與遠程服務器的連接也可以是對象。當實物被抽象成對象,實物之間的關系就變成了對象之間的關系,從而就可以模擬現實情況,針對對象進行編程

對象是一個容器,封裝了屬性(property)和方法(method)

屬性是對象的狀態,方法是對象的行為(完成某種任務)。比如,我們可以把動物抽象為animal對象,使用“屬性”記錄具體是那一種動物,使用“方法”表示動物的某種行為(奔跑、捕獵、休息等等)

構造函數

面向對象編程的第一步,就是要生成對象。前面說過,對象是單個實物的抽象。通常需要一個模板,表示某一類實物的共同特征,然后對象根據這個模板生成。典型的面向對象編程語言(比如 C++ 和 Java),都有“類”(class)這個概念。所謂“類”就是對象的模板,對象就是“類”的實例。但是,JavaScript 語言的對象體系,不是基于“類”的,而是基于構造函數(constructor)和原型鏈(prototype)。JavaScript 語言使用構造函數(constructor)作為對象的模板。所謂”構造函數”,就是專門用來生成實例對象的函數。它就是對象的模板,描述實例對象的基本結構。一個構造函數,可以生成多個實例對象,這些實例對象都有相同的結構。構造函數就是一個普通的函數,但是有自己的特征和用法

var Vehicle = function () {this.price = 1000; };

上面代碼中,Vehicle就是構造函數。為了與普通函數區別,構造函數名字的第一個字母通常大寫

構造函數的特點有兩個。

1.函數體內部使用了this關鍵字,代表了所要生成的對象實例

2.生成對象的時候,必須使用new命令

new 命令

基本用法

new命令的作用,就是執行構造函數,返回一個實例對象

var Vehicle = function () {this.price = 1000; }; var v = new Vehicle(); v.price // 1000

上面代碼通過new命令,讓構造函數Vehicle生成一個實例對象,保存在變量v中。這個新生成的實例對象,從構造函數Vehicle得到了price屬性。new命令執行時,構造函數內部的this,就代表了新生成的實例對象,this.price表示實例對象有一個price屬性,值是1000。使用new命令時,根據需要構造函數也可以接受參數

var Vehicle = function (p) {this.price = p; }; var v = new Vehicle(500);

new命令本身就可以執行構造函數,所以后面的構造函數可以帶括號,也可以不帶括號。下面兩行代碼是等價的,但是為了表示這里是函數調用,推薦使用括號

var v = new Vehicle(); // 推薦的寫法 var v = new Vehicle; // 不推薦的寫法

一個很自然的問題是,如果忘了使用new命令,直接調用構造函數會發生什么事?這種情況下,構造函數就變成了普通函數,并不會生成實例對象。而且由于后面會說到的原因,this這時代表全局對象,將造成一些意想不到的結果

var Vehicle = function (){this.price = 1000; }; var v = Vehicle(); v // undefined price // 1000

上面代碼中,調用Vehicle構造函數時,忘了加上new命令。結果,變量v變成了undefined,而price屬性變成了全局變量。因此,應該非常小心,避免不使用new命令、直接調用構造函數。為了保證構造函數必須與new命令一起使用,一個解決辦法是,構造函數內部使用嚴格模式,即第一行加上use strict。這樣的話,一旦忘了使用new命令,直接調用構造函數就會報錯

function Fubar(foo, bar){'use strict';this._foo = foo;this._bar = bar; } Fubar() // TypeError: Cannot set property '_foo' of undefined

上面代碼的Fubar為構造函數,use strict命令保證了該函數在嚴格模式下運行。由于嚴格模式中,函數內部的this不能指向全局對象,默認等于undefined,導致不加new調用會報錯(JavaScript 不允許對undefined添加屬性)。另一個解決辦法,構造函數內部判斷是否使用new命令,如果發現沒有使用,則直接返回一個實例對象

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

上面代碼中的構造函數,不管加不加new命令,都會得到同樣的結果

new 命令的原理

使用new命令時,它后面的函數依次執行下面的步驟

1.創建一個空對象,作為將要返回的對象實例

2.將這個空對象的原型,指向構造函數的prototype屬性

3.將這個空對象賦值給函數內部的this關鍵字

4.開始執行構造函數內部的代碼

也就是說,構造函數內部,this指的是一個新生成的空對象,所有針對this的操作,都會發生在這個空對象上。構造函數之所以叫“構造函數”,就是說這個函數的目的,就是操作一個空對象(即this對象),將其“構造”為需要的樣子。如果構造函數內部有return語句,而且return后面跟著一個對象,new命令會返回return語句指定的對象;否則,就會不管return語句,返回this對象

var Vehicle = function () {this.price = 1000;return 1000; }; (new Vehicle()) === 1000 // false

上面代碼中,構造函數Vehicle的return語句返回一個數值。這時,new命令就會忽略這個return語句,返回“構造”后的this對象。但是,如果return語句返回的是一個跟this無關的新對象,new命令會返回這個新對象,而不是this對象。這一點需要特別引起注意

var Vehicle = function (){this.price = 1000;return { price: 2000 }; }; (new Vehicle()).price // 2000

上面代碼中,構造函數Vehicle的return語句,返回的是一個新對象。new命令會返回這個對象,而不是this對象。另一方面,如果對普通函數(內部沒有this關鍵字的函數)使用new命令,則會返回一個空對象

function getMessage() {return 'this is a message'; } var msg = new getMessage(); msg // {} typeof msg // "object"

上面代碼中,getMessage是一個普通函數,返回一個字符串。對它使用new命令,會得到一個空對象。這是因為new命令總是返回一個對象,要么是實例對象,要么是return語句指定的對象。本例中,return語句返回的是字符串,所以new命令就忽略了該語句。new命令簡化的內部流程,可以用下面的代碼表示

function _new(/* 構造函數 */ constructor, /* 構造函數參數 */ params) { var args = [].slice.call(arguments); // 將 arguments 對象轉為數組 var constructor = args.shift(); // 取出構造函數 var context = Object.create(constructor.prototype); // 創建一個空對象,繼承構造函數的 prototype 屬性 var result = constructor.apply(context, args); // 執行構造函數 return (typeof result === 'object' && result != null) ? result : context; // 如果返回結果是對象,就直接返回,否則返回 context 對象 } var actor = _new(Person, '張三', 28); // 實例

new.target

函數內部可以使用new.target屬性。如果當前函數是new命令調用,new.target指向當前函數,否則為undefined

function f() {console.log(new.target === f); } f() // false new f() // true

使用這個屬性,可以判斷函數調用的時候,是否使用new命令

function f() {if (!new.target) {throw new Error('請使用 new 命令調用!');}// ... } f() // Uncaught Error: 請使用 new 命令調用!

Object.create() 創建實例對象

構造函數作為模板,可以生成實例對象。但是,有時拿不到構造函數,只能拿到一個現有的對象。我們希望以這個現有的對象作為模板,生成新的實例對象,這時就可以使用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 張三.

上面代碼中,對象person1是person2的模板,后者繼承了前者的屬性和方法

this 關鍵字

涵義

this關鍵字是一個非常重要的語法點。毫不夸張地說,不理解它的含義大部分開發任務都無法完成。this可以用在構造函數之中,表示實例對象;除此之外,this還可以用在別的場合。但不管是什么場合,this都有一個共同點:它總是返回一個對象;簡單說,this就是屬性或方法“當前”所在的對象

var person = {name: '張三',describe: function () { return '姓名:'+ this.name; } }; person.describe() // "姓名:張三"

上面代碼中,this.name表示name屬性所在的那個對象。由于this.name是在describe方法中調用,而describe方法所在的當前對象是person,因此this指向person,this.name就是person.name。由于對象的屬性可以賦給另一個對象,所以屬性所在的當前對象是可變的,即this的指向是可變的

var A = {name: '張三',describe: function () { return '姓名:'+ this.name; } }; var B = { name: '李四' }; B.describe = A.describe; B.describe() // "姓名:李四"

上面代碼中,A.describe屬性被賦給B,于是B.describe就表示describe方法所在的當前對象是B,所以this.name就指向B.name。稍稍重構這個例子,this的動態指向就能看得更清楚

function f() { return '姓名:'+ this.name; } var A = { name: '張三', describe: f }; var B = { name: '李四', describe: f }; A.describe() // "姓名:張三" B.describe() // "姓名:李四"

只要函數被賦給另一個變量,this的指向就會變

var A = {name: '張三',describe: function () { return '姓名:'+ this.name; } }; var name = '李四'; var f = A.describe; f() // "姓名:李四"

上面代碼中,A.describe被賦值給變量f,內部的this就會指向f運行時所在的對象(本例是頂層對象)。再看一個網頁編程的例子

<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>

上面代碼是一個文本輸入框,每當用戶輸入一個值,就會調用onChange回調函數,驗證這個值是否在指定范圍。瀏覽器會向回調函數傳入當前對象,因此this就代表傳入當前對象(即文本框),然后就可以從this.value上面讀到用戶的輸入值

總結一下,JavaScript 語言之中,一切皆對象,運行環境也是對象,所以函數都是在某個對象之中運行,this就是函數運行時所在的對象(環境)。這本來并不會讓用戶糊涂,但是 JavaScript 支持運行環境動態切換,也就是說,this的指向是動態的,沒有辦法事先確定到底指向哪個對象,這才是最讓初學者感到困惑的地方

實質

JavaScript 語言之所以有 this 的設計,跟內存里面的數據結構有關系

var obj = { foo: 5 };

上面的代碼將一個對象賦值給變量obj。JavaScript 引擎會先在內存里面,生成一個對象{ foo: 5 },然后把這個對象的內存地址賦值給變量obj。也就是說,變量obj是一個地址(reference)。后面如果要讀取obj.foo,引擎先從obj拿到內存地址,然后再從該地址讀出原始的對象,返回它的foo屬性。原始的對象以字典結構保存,每一個屬性名都對應一個屬性描述對象。舉例來說,上面例子的foo屬性,實際上是以下面的形式保存的

{foo: {[[value]]: 5[[writable]]: true[[enumerable]]: true[[configurable]]: true} }

注意,foo屬性的值保存在屬性描述對象的value屬性里面。這樣的結構是很清晰的,問題在于屬性的值可能是一個函數;這時,引擎會將函數單獨保存在內存中,然后再將函數的地址賦值給foo屬性的value屬性

var obj = { foo: function () {} };

由于函數是一個單獨的值,所以它可以在不同的環境(上下文)執行

var f = function () {}; var obj = { f: f }; f() // 單獨執行 obj.f() // obj 環境執行

JavaScript 允許在函數體內部,引用當前環境的其他變量

var f = function () {console.log(x); };

上面代碼中,函數體里面使用了變量x,該變量由運行環境提供。現在問題就來了,由于函數可以在不同的運行環境執行,所以需要有一種機制,能夠在函數體內部獲得當前的運行環境(context)。所以,this就出現了,它的設計目的就是在函數體內部,指代函數當前的運行環境

var f = function () { console.log(this.x);} var x = 1; var obj = { f: f, x: 2}; // 單獨執行 f() // 1 // obj 環境執行 obj.f() // 2

上面代碼中,函數f在全局環境執行,this.x指向全局環境的x;在obj環境執行,this.x指向obj.x

使用場合

this主要有以下幾個使用場合

全局環境

全局環境使用this,它指的就是頂層對象window

this === window // true function f() {console.log(this === window); } f() // true

上面代碼說明,不管是不是在函數內部,只要是在全局環境下運行,this就是指頂層對象window

構造函數

構造函數中的this,指的是實例對象

var Obj = function (p) {this.p = p; }; var o = new Obj('Hello World!'); o.p // "Hello World!"

上面代碼定義了一個構造函數Obj。由于this指向實例對象,所以在構造函數內部定義this.p,就相當于定義實例對象有一個p屬性

對象的方法

如果對象的方法里面包含this,this的指向就是方法運行時所在的對象。該方法賦值給另一個對象,就會改變this的指向。但是,這條規則很不容易把握。請看下面的代碼

var obj ={foo: function () { console.log(this); } }; obj.foo() // obj

上面代碼中,obj.foo方法執行時,它內部的this指向obj。但是,下面這幾種用法,都會改變this的指向

// 情況一 (obj.foo = obj.foo)() // window // 情況二 (false || obj.foo)() // window // 情況三 (1, obj.foo)() // window

上面代碼中,obj.foo就是一個值。這個值真正調用的時候,運行環境已經不是obj了,而是全局環境,所以this不再指向obj。可以這樣理解,JavaScript 引擎內部,obj和obj.foo儲存在兩個內存地址,稱為地址一和地址二。obj.foo()這樣調用時,是從地址一調用地址二,因此地址二的運行環境是地址一,this指向obj。但是,上面三種情況,都是直接取出地址二進行調用,這樣的話,運行環境就是全局環境,因此this指向全局環境。上面三種情況等同于下面的代碼

// 情況一 (obj.foo = function () { console.log(this);})() // 等同于 (function () { console.log(this);})() // 情況二 (false || function () { console.log(this);})() // 情況三 (1, function () { console.log(this);})()

如果this所在的方法不在對象的第一層,這時this只是指向當前一層的對象,而不會繼承更上面的層

var a = {p: 'Hello',b: {m: function() { console.log(this.p); }} }; a.b.m() // undefined

上面代碼中,a.b.m方法在a對象的第二層,該方法內部的this不是指向a,而是指向a.b,因為實際執行的是下面的代碼

var b = {m: function() { console.log(this.p); } }; var a = { p: 'Hello', b: b }; (a.b).m() // 等同于 b.m()

如果要達到預期效果,只有寫成下面這樣

var a = {b: {m: function() { console.log(this.p); },p: 'Hello'} };

如果這時將嵌套對象內部的方法賦值給一個變量,this依然會指向全局對象

var a = {b: {m: function() { console.log(this.p); },p: 'Hello'} }; var hello = a.b.m; hello() // undefined

上面代碼中,m是多層對象內部的一個方法。為求簡便,將其賦值給hello變量,結果調用時,this指向了頂層對象。為了避免這個問題,可以只將m所在的對象賦值給hello,這樣調用時,this的指向就不會變

var hello = a.b; hello.m() // Hello

使用注意點

避免多層 this

由于this的指向是不確定的,所以切勿在函數中包含多層的this

var o = {f1: function () {console.log(this);var f2 = function () { console.log(this); }();} } o.f1() // Object // Window

上面代碼包含兩層this,結果運行后,第一層指向對象o,第二層指向全局對象,因為實際執行的是下面的代碼

var temp = function () { console.log(this); }; var o = {f1: function () { console.log(this); var f2 = temp(); } }

一個解決方法是在第二層改用一個指向外層this的變量

var o = {f1: function() {console.log(this);var that = this;var f2 = function() { console.log(that); }();} } o.f1() // Object // Object

上面代碼定義了變量that,固定指向外層的this,然后在內層使用that,就不會發生this指向的改變。事實上,使用一個變量固定this的值,然后內層函數調用這個變量,是非常常見的做法。JavaScript 提供了嚴格模式,也可以硬性避免這種問題。嚴格模式下,如果函數內部的this指向頂層對象,就會報錯

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'聲明采用嚴格模式,這時內部的this一旦指向頂層對象,就會報錯

避免數組處理方法中的 this

數組的map和foreach方法,允許提供一個函數作為參數。這個函數內部不應該使用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方法的回調函數中的this,其實是指向window對象,因此取不到o.v的值。原因跟上一段的多層this是一樣的,就是內層的this不指向外部,而指向頂層對象。解決這個問題的一種方法,就是前面提到的,使用中間變量固定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當作foreach方法的第二個參數,固定它的運行環境

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

避免回調函數中的 this

回調函數中的this往往會改變指向,最好避免使用

var o = new Object(); o.f = function () { console.log(this === o);} $('#button').on('click', o.f); // jQuery 的寫法

上面代碼中,點擊按鈕以后,控制臺會顯示false。原因是此時this不再指向o對象,而是指向按鈕的 DOM 對象,因為f方法是在按鈕對象的環境中被調用的。這種細微的差別,很容易在編程中忽視,導致難以察覺的錯誤。為了解決這個問題,可以采用下面的一些方法對this進行綁定,也就是使得this固定指向某個對象,減少不確定性

綁定 this 的方法

this的動態切換,固然為 JavaScript 創造了巨大的靈活性,但也使得編程變得困難和模糊。有時,需要把this固定下來,避免出現意想不到的情況。JavaScript 提供了call、apply、bind這三個方法,來切換/固定this的指向

Function.prototype.call()

函數實例的call方法,可以指定函數內部this的指向(即函數執行時所在的作用域),然后在所指定的作用域中,調用該函數

var obj = {}; var f = function () { return this; }; f() === window // true f.call(obj) === obj // true

上面代碼中,全局環境運行函數f時,this指向全局環境(瀏覽器為window對象);call方法可以改變this的指向,指定this指向對象obj,然后在對象obj的作用域中運行函數f。call方法的參數,應該是一個對象。如果參數為空、null和undefined,則默認傳入全局對象

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函數中的this關鍵字,如果指向全局對象,返回結果為123。如果使用call方法將this關鍵字指向obj對象,返回結果為456。可以看到,如果call方法沒有參數,或者參數為null或undefined,則等同于指向全局對象。如果call方法的參數是一個原始值,那么這個原始值會自動轉成對應的包裝對象,然后傳入call方法

var f = function () {return this; }; f.call(5) // Number {[[PrimitiveValue]]: 5}

上面代碼中,call的參數為5,不是對象,會被自動轉成包裝對象(Number的實例),綁定f內部的this。call方法還可以接受多個參數

func.call(thisValue, arg1, arg2, ...)

call的第一個參數就是this所要指向的那個對象,后面的參數則是函數調用時所需的參數

function add(a, b) { return a + b;} add.call(this, 1, 2) // 3

上面代碼中,call方法指定函數add內部的this綁定當前環境(對象),并且參數為1和2,因此函數add運行后得到3。call方法的一個應用是調用對象的原生方法

var obj = {}; // object的hasOwnProperty()方法返回一個布爾值,判斷對象是否包含特定的自身(非繼承)屬性 obj.hasOwnProperty('toString') // false // 覆蓋掉繼承的 hasOwnProperty 方法 obj.hasOwnProperty = function () { return true;}; obj.hasOwnProperty('toString') // true Object.prototype.hasOwnProperty.call(obj, 'toString') // false

上面代碼中,hasOwnProperty是obj對象繼承的方法,如果這個方法一旦被覆蓋,就不會得到正確結果。call方法可以解決這個問題,它將hasOwnProperty方法的原始定義放到obj對象上執行,這樣無論obj上有沒有同名方法,都不會影響結果

Function.prototype.apply()

apply方法的作用與call方法類似,也是改變this指向,然后再調用該函數。唯一的區別就是,它接收一個數組作為函數執行時的參數,使用格式如下:

func.apply(thisValue, [arg1, arg2, ...])

apply方法的第一個參數也是this所要指向的那個對象,如果設為null或undefined,則等同于指定全局對象。第二個參數則是一個數組,該數組的所有成員依次作為參數,傳入原函數。原函數的參數,在call方法中必須一個個添加,但是在apply方法中,必須以數組形式添加

function f(x, y){ console.log(x + y);} f.call(null, 1, 1) // 2 f.apply(null, [1, 1]) // 2

上面代碼中,f函數本來接受兩個參數,使用apply方法以后,就變成可以接受一個數組作為參數。利用這一點,可以做一些有趣的應用

找出數組最大元素

JavaScript 不提供找出數組最大元素的函數。結合使用apply方法和Math.max方法,就可以返回數組的最大元素

var a = [10, 2, 4, 15, 9]; Math.max.apply(null, a) // 15
將數組的空元素變為undefined

通過apply方法,利用Array構造函數將數組的空元素變成undefined

Array.apply(null, ['a', ,'b']) // [ 'a', undefined, 'b' ]

空元素與undefined的差別在于,數組的forEach方法會跳過空元素,但是不會跳過undefined。因此,遍歷內部元素的時候,會得到不同的結果

var a = ['a', , 'b']; function print(i) { console.log(i);} a.forEach(print) // a // b Array.apply(null, a).forEach(print) // a // undefined // b
轉換類似數組的對象

另外,利用數組對象的slice方法,可以將一個類似數組的對象(比如arguments對象)轉為真正的數組

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方法的參數都是對象,但是返回結果都是數組,這就起到了將對象轉成數組的目的。從上面代碼可以看到,這個方法起作用的前提是,被處理的對象必須有length屬性,以及相對應的數字鍵

綁定回調函數的對象

前面的按鈕點擊事件的例子,可以改寫如下:

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 的寫法

上面代碼中,點擊按鈕以后,控制臺將會顯示true。由于apply方法(或者call方法)不僅綁定函數執行時所在的對象,還會立即執行函數,因此不得不把綁定語句寫在一個函數體內。更簡潔的寫法是采用下面介紹的bind方法

Function.prototype.bind()

bind方法用于將函數體內的this綁定到某個對象,然后返回一個新函數

var d = new Date(); d.getTime() // 1553063951536 var print = d.getTime; print() // Uncaught TypeError: this is not a Date object.

上面代碼中,我們將d.getTime方法賦給變量print,然后調用print就報錯了。這是因為getTime方法內部的this,綁定Date對象的實例,賦給變量print以后,內部的this已經不指向Date對象的實例了。bind方法可以解決這個問題

var print = d.getTime.bind(d); print() // 1553063951536

上面代碼中,bind方法將getTime方法內部的this綁定到d對象,這時就可以安全地將這個方法賦值給其他變量了。bind方法的參數就是所要綁定this的對象,下面是一個更清晰的例子

var counter = {count: 0,inc: function () { this.count++; } }; var func = counter.inc.bind(counter); func(); counter.count // 1

上面代碼中,counter.inc方法被賦值給變量func。這時必須用bind方法將inc內部的this,綁定到counter,否則就會出錯。this綁定到其他對象也是可以的

var counter = {count: 0,inc: function () { this.count++; } }; var obj = { count: 100 }; var func = counter.inc.bind(obj); func(); obj.count // 101

上面代碼中,bind方法將inc方法內部的this,綁定到obj對象。結果調用func函數以后,遞增的就是obj內部的count屬性。bind還可以接受更多的參數,將這些參數綁定原函數的參數

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對象,還將add函數的第一個參數x綁定成5,然后返回一個新函數newAdd,這個函數只要再接受一個參數y就能運行了。如果bind方法的第一個參數是null或undefined,等于將this綁定到全局對象,函數運行時this指向頂層對象(瀏覽器為window)

function add(x, y) { return x + y;} var plus = add.bind(null, 5); plus(10) // 15

上面代碼中,函數add內部并沒有this,使用bind方法的主要目的是綁定參數x,以后每次運行新函數plus,就只需要提供另一個參數y就夠了。而且因為add內部沒有this,所以bind的第一個參數是null,不過這里如果是其他對象,也沒有影響。bind方法有一些使用注意點

每一次返回一個新函數

bind方法每運行一次,就返回一個新函數,這會產生一些問題。比如,監聽事件的時候,不能寫成下面這樣

element.addEventListener('click', o.m.bind(o));

上面代碼中,click事件綁定bind方法生成的一個匿名函數。這樣會導致無法取消綁定,所以,下面的代碼是無效的

element.removeEventListener('click', o.m.bind(o));

正確的方法是寫成下面這樣:

var listener = o.m.bind(o); element.addEventListener('click', listener); // ... element.removeEventListener('click', listener);
結合回調函數使用

回調函數是 JavaScript 最常用的模式之一,但是一個常見的錯誤是,將包含this的方法直接當作回調函數。解決方法就是使用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方法會調用回調函數。這時如果直接把counter.inc傳入,調用時counter.inc內部的this就會指向全局對象。使用bind方法將counter.inc綁定counter以后,就不會有這個問題,this總是指向counter。還有一種情況比較隱蔽,就是某些數組方法可以接受一個函數當作參數。這些函數內部的this指向,很可能也會出錯

var obj = {name: '張三',times: [1, 2, 3],print: function () {this.times.forEach(function (n) { console.log(this.name); });} }; obj.print() // 沒有任何輸出

上面代碼中,obj.print內部this.times的this是指向obj的,這個沒有問題。但是,forEach方法的回調函數內部的this.name卻是指向全局對象,導致沒有辦法取到值。稍微改動一下,就可以看得更清楚

obj.print = function () {this.times.forEach(function (n) { console.log(this === window); }); }; obj.print() // true // true // true

解決這個問題,也是通過bind方法綁定this

obj.print = function () {this.times.forEach(function (n) { console.log(this.name); }.bind(this)); }; obj.print() // 張三 // 張三 // 張三
結合call方法使用

利用bind方法,可以改寫一些 JavaScript 原生方法的使用形式,以數組的slice方法為例

[1, 2, 3].slice(0, 1) // [1] // 等同于 Array.prototype.slice.call([1, 2, 3], 0, 1) // [1]

上面的代碼中,數組的slice方法從[1, 2, 3]里面,按照指定位置和長度切分出另一個數組。這樣做的本質是在[1, 2, 3]上面調用Array.prototype.slice方法,因此可以用call方法表達這個過程,得到同樣的結果。call方法實質上是調用Function.prototype.call方法,因此上面的表達式可以用bind方法改寫

var slice = Function.prototype.call.bind(Array.prototype.slice); slice([1, 2, 3], 0, 1) // [1]

上面代碼的含義就是,將Array.prototype.slice變成Function.prototype.call方法所在的對象,調用時就變成了Array.prototype.slice.call。類似的寫法還可以用于其他數組方法

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]

如果再進一步,將Function.prototype.call方法綁定到Function.prototype.bind對象,就意味著bind的調用形式也可以被改寫

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方法就可以直接使用,不需要在函數實例上使用

對象的繼承

面向對象編程很重要的一個方面,就是對象的繼承。A 對象通過繼承 B 對象,就能直接擁有 B 對象的所有屬性和方法。這對于代碼的復用是非常有用的。大部分面向對象的編程語言,都是通過“類”(class)實現對象的繼承。傳統上,JavaScript 語言的繼承不通過 class,而是通過“原型對象”(prototype)實現,這里主要介紹 JavaScript 的原型鏈繼承。ES6 引入了 class 語法,基于 class 的繼承暫不在這里介紹

原型對象概述

構造函數的缺點

JavaScript 通過構造函數生成新對象,因此構造函數可以視為對象的模板。實例對象的屬性和方法,可以定義在構造函數內部

function Cat (name, color) { this.name = name; this.color = color;} var cat1 = new Cat('大毛', '白色'); cat1.name // '大毛' cat1.color // '白色'

上面代碼中,Cat函數是一個構造函數,函數內部定義了name屬性和color屬性,所有實例對象(上例是cat1)都會生成這兩個屬性,即這兩個屬性會定義在實例對象上面。通過構造函數為實例對象定義屬性,雖然很方便,但是有一個缺點。同一個構造函數的多個實例之間,無法共享屬性,從而造成對系統資源的浪費

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是同一個構造函數的兩個實例,它們都具有meow方法。由于meow方法是生成在每個實例對象上面,所以兩個實例就生成了兩次。也就是說,每新建一個實例,就會新建一個meow方法。這既沒有必要,又浪費系統資源,因為所有meow方法都是同樣的行為,完全應該共享。這個問題的解決方法,就是 JavaScript 的原型對象(prototype)

prototype 屬性的作用

JavaScript 繼承機制的設計思想就是,原型對象的所有屬性和方法,都能被實例對象共享。也就是說,如果屬性和方法定義在原型上,那么所有實例對象就能共享,不僅節省了內存,還體現了實例對象之間的聯系。下面,先看怎么為對象指定原型。JavaScript 規定,每個函數都有一個prototype屬性,指向一個對象

function f() {} typeof f.prototype // "object"

上面代碼中,函數f默認具有prototype屬性,指向一個對象。對于普通函數來說,該屬性基本無用。但是,對于構造函數來說,生成實例的時候,該屬性會自動成為實例對象的原型

function f() {} typeof f.prototype // "object"

上面代碼中,函數f默認具有prototype屬性,指向一個對象。對于普通函數來說,該屬性基本無用。但是,對于構造函數來說,生成實例的時候,該屬性會自動成為實例對象的原型

function Animal(name) { this.name = name;} Animal.prototype.color = 'white'; var cat1 = new Animal('大毛'); var cat2 = new Animal('二毛'); cat1.color // 'white' cat2.color // 'white'

上面代碼中,構造函數Animal的prototype屬性,就是實例對象cat1和cat2的原型對象。原型對象上添加一個color屬性,結果,實例對象都共享了該屬性。原型對象的屬性不是實例對象自身的屬性。只要修改原型對象,變動就立刻會體現在所有實例對象上。也就是說,當實例對象本身沒有某個屬性或方法的時候,它會到原型對象去尋找該屬性或方法。這就是原型對象的特殊之處。如果實例對象自身就有某個屬性或方法,它就不會再去原型對象尋找這個屬性或方法

Animal.prototype.color = 'yellow'; cat1.color = 'black'; cat1.color // 'black' cat2.color // 'yellow' Animal.prototype.color // 'yellow';

總結一下,原型對象的作用,就是定義所有實例對象共享的屬性和方法。這也是它被稱為原型對象的原因,而實例對象可以視作從原型對象衍生出來的子對象

Animal.prototype.walk = function () {console.log(this.name + ' is walking'); };

上面代碼中,Animal.prototype對象上面定義了一個walk方法,這個方法將可以在所有Animal實例對象上面調用

原型鏈

JavaScript 規定,所有對象都有自己的原型對象(prototype)。一方面,任何一個對象,都可以充當其他對象的原型;另一方面,由于原型對象也是對象,所以它也有自己的原型。因此,就會形成一個“原型鏈”(prototype chain):對象到原型,再到原型的原型……。如果一層層地上溯,所有對象的原型最終都可以上溯到Object.prototype,即Object構造函數的prototype屬性。也就是說,所有對象都繼承了Object.prototype的屬性。這就是所有對象都有valueOf和toString方法的原因,因為這是從Object.prototype繼承的。那么,Object.prototype對象有沒有它的原型呢?回答是Object.prototype的原型是null。null沒有任何屬性和方法,也沒有自己的原型。因此,原型鏈的盡頭就是null

Object.getPrototypeOf(Object.prototype) // null

上面代碼表示,Object.prototype對象的原型是null,由于null沒有任何屬性,所以原型鏈到此為止。Object.getPrototypeOf方法返回參數對象的原型。讀取對象的某個屬性時,JavaScript 引擎先尋找對象本身的屬性,如果找不到,就到它的原型去找,如果還是找不到,就到原型的原型去找。如果直到最頂層的Object.prototype還是找不到,則返回undefined。如果對象自身和它的原型,都定義了一個同名屬性,那么優先讀取對象自身的屬性,這叫做“覆蓋”(overriding)

注意,一級級向上,在整個原型鏈上尋找某個屬性,對性能是有影響的。所尋找的屬性在越上層的原型對象,對性能的影響越大。如果尋找某個不存在的屬性,將會遍歷整個原型鏈。舉例來說,如果讓構造函數的prototype屬性指向一個數組,就意味著實例對象可以調用數組方法

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是構造函數MyArray的實例對象,由于MyArray.prototype指向一個數組實例,使得mine可以調用數組方法(這些方法定義在數組實例的prototype對象上面)。最后那行instanceof表達式,用來比較一個對象是否為某個構造函數的實例,結果就是證明mine為Array的實例。上面代碼還出現了原型對象的constructor屬性

constructor 屬性

prototype對象有一個constructor屬性,默認指向prototype對象所在的構造函數

function P() {} P.prototype.constructor === P // true

由于constructor屬性定義在prototype對象上面,意味著可以被所有實例對象繼承

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是構造函數P的實例對象,但是p自身沒有constructor屬性,該屬性其實是讀取原型鏈上面的P.prototype.constructor屬性。constructor屬性的作用是,可以得知某個實例對象,到底是哪一個構造函數產生的

function F() {}; var f = new F(); f.constructor === F // true f.constructor === RegExp // false

上面代碼中,constructor屬性確定了實例對象f的構造函數是F,而不是RegExp。另一方面,有了constructor屬性,就可以從一個實例對象新建另一個實例

function Constr() {} var x = new Constr(); var y = new x.constructor(); y instanceof Constr // true

上面代碼中,x是構造函數Constr的實例,可以從x.constructor間接調用構造函數。這使得在實例方法中,調用自身的構造函數成為可能

Constr.prototype.createCopy = function () {return new this.constructor(); };

上面代碼中,createCopy方法調用構造函數,新建另一個實例。constructor屬性表示原型對象與構造函數之間的關聯關系,如果修改了原型對象,一般會同時修改constructor屬性,防止引用的時候出錯

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

上面代碼中,構造函數Person的原型對象改掉了,但是沒有修改constructor屬性,導致這個屬性不再指向Person。由于Person的新原型是一個普通對象,而普通對象的constructor屬性指向Object構造函數,導致Person.prototype.constructor變成了Object。所以,修改原型對象時,一般要同時修改constructor屬性的指向

// 壞的寫法 C.prototype = {method1: function (...) { ... }, }; // 好的寫法 C.prototype = {constructor: C,method1: function (...) { ... }, }; // 更好的寫法 C.prototype.method1 = function (...) { ... };

上面代碼中,要么將constructor屬性重新指向原來的構造函數,要么只在原型對象上添加方法,這樣可以保證instanceof運算符不會失真。如果不能確定constructor屬性是什么函數,還有一個辦法:通過name屬性,從實例得到構造函數的名稱

function Foo() {} var f = new Foo(); f.constructor.name // "Foo"

instanceof 運算符

instanceof運算符返回一個布爾值,表示對象是否為某個構造函數的實例

var v = new Vehicle(); v instanceof Vehicle // true

上面代碼中,對象v是構造函數Vehicle的實例,所以返回true。instanceof運算符的左邊是實例對象,右邊是構造函數。它會檢查右邊構建函數的原型對象(prototype),是否在左邊對象的原型鏈上。因此,下面兩種寫法是等價的

v instanceof Vehicle // 等同于 Vehicle.prototype.isPrototypeOf(v)

由于instanceof檢查整個原型鏈,因此同一個實例對象,可能會對多個構造函數都返回true

var d = new Date(); d instanceof Date // true d instanceof Object // true

上面代碼中,d同時是Date和Object的實例,因此對這兩個構造函數都返回true。instanceof的原理是檢查右邊構造函數的prototype屬性,是否在左邊對象的原型鏈上。有一種特殊情況,就是左邊對象的原型鏈上,只有null對象。這時,instanceof判斷會失真

var obj = Object.create(null); typeof obj // "object" Object.create(null) instanceof Object // false

上面代碼中,Object.create(null)返回一個新對象obj,它的原型是null。右邊的構造函數Object的prototype屬性,不在左邊的原型鏈上,因此instanceof就認為obj不是Object的實例。但是,只要一個對象的原型不是null,instanceof運算符的判斷就不會失真。instanceof運算符的一個用處,是判斷值的類型

var x = [1, 2, 3]; var y = {}; x instanceof Array // true y instanceof Object // true

注意,instanceof運算符只能用于對象,不適用原始類型的值。此外,對于undefined和null,instanceOf運算符總是返回false

var s = 'hello'; s instanceof String // false undefined instanceof Object // false null instanceof Object // false

利用instanceof運算符,還可以巧妙地解決,調用構造函數時,忘了加new命令的問題

function Fubar (foo, bar) {if (this instanceof Fubar) {this._foo = foo;this._bar = bar;} else {return new Fubar(foo, bar);} }

上面代碼使用instanceof運算符,在函數體內部判斷this關鍵字是否為構造函數Fubar的實例。如果不是,就表明忘了加new命令

構造函數的繼承

讓一個構造函數繼承另一個構造函數,是非常常見的需求。這可以分成兩步實現

第一步:在子類的構造函數中,調用父類的構造函數

function Sub(value) {Super.call(this);this.prop = value; }

上面代碼中,Sub是子類的構造函數,this是子類的實例。在實例上調用父類的構造函數Super,就會讓子類實例具有父類實例的屬性。

第二步,是讓子類的原型指向父類的原型,這樣子類就可以繼承父類原型

Sub.prototype = Object.create(Super.prototype); Sub.prototype.constructor = Sub; Sub.prototype.method = '...';

上面代碼中,Sub.prototype是子類的原型,要將它賦值為Object.create(Super.prototype),而不是直接等于Super.prototype。否則后面兩行對Sub.prototype的操作,會連父類的原型Super.prototype一起修改掉。

另外一種寫法是Sub.prototype等于一個父類實例

Sub.prototype = new Super();

上面這種寫法也有繼承的效果,但是子類會具有父類實例的方法。有時,這可能不是我們需要的,所以不推薦使用這種寫法。舉例來說,下面是一個Shape構造函數

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構造函數繼承Shape

// 第一步,子類繼承父類的實例 function Rectangle() {Shape.call(this); // 調用父類構造函數 } // 另一種寫法 function Rectangle() {this.base = Shape;this.base(); } // 第二步,子類繼承父類的原型 Rectangle.prototype = Object.create(Shape.prototype); Rectangle.prototype.constructor = Rectangle;

采用這樣的寫法以后,instanceof運算符會對子類和父類的構造函數,都返回true

var rect = new Rectangle(); rect instanceof Rectangle // true rect instanceof Shape // true

上面代碼中,子類是整體繼承父類。有時只需要單個方法的繼承,這時可以采用下面的寫法

ClassB.prototype.print = function() {ClassA.prototype.print.call(this);// some code }

上面代碼中,子類B的print方法先調用父類A的print方法,再部署自己的代碼。這就等于繼承了父類A的print方法

多重繼承

JavaScript 不提供多重繼承功能,即不允許一個對象同時繼承多個對象。但是,可以通過變通方法,實現這個功能

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; // 指定構造函數 var s = new S(); s.hello // 'hello' s.world // 'world'

上面代碼中,子類S同時繼承了父類M1和M2。這種模式又稱為 Mixin(混入)

模塊

隨著網站逐漸變成“互聯網應用程序”,嵌入網頁的 JavaScript 代碼越來越龐大,越來越復雜。網頁越來越像桌面程序,需要一個團隊分工協作、進度管理、單元測試等等……開發者必須使用軟件工程的方法,管理網頁的業務邏輯。JavaScript 模塊化編程,已經成為一個迫切的需求。理想情況下,開發者只需要實現核心的業務邏輯,其他都可以加載別人已經寫好的模塊。但是,JavaScript 不是一種模塊化編程語言,ES6 才開始支持“類”和“模塊”。下面介紹傳統的做法,如何利用對象實現模塊的效果

基本的實現方法

模塊是實現特定功能的一組屬性和方法的封裝。簡單的做法是把模塊寫成一個對象,所有的模塊成員都放到這個對象里面

var module1 = new Object({_count : 0,m1 : function (){//...},m2 : function (){//...} });

上面的函數m1和m2,都封裝在module1對象里。使用的時候,就是調用這個對象的屬性

module1.m1();

但是,這樣的寫法會暴露所有模塊成員,內部狀態可以被外部改寫。比如,外部代碼可以直接改變內部計數器的值

module1._count = 5;

封裝私有變量:構造函數的寫法

我們可以利用構造函數,封裝私有變量

function StringBuilder() {var buffer = [];this.add = function (str) {buffer.push(str);};this.toString = function () {return buffer.join('');}; }

上面代碼中,buffer是模塊的私有變量。一旦生成實例對象,外部是無法直接訪問buffer的。但是,這種方法將私有變量封裝在構造函數中,導致構造函數與實例對象是一體的,總是存在于內存之中,無法在使用完成后清除。這意味著,構造函數有雙重作用,既用來塑造實例對象,又用來保存實例對象的數據,違背了構造函數與實例對象在數據上相分離的原則(即實例對象的數據,不應該保存在實例對象以外)。同時,非常耗費內存

function StringBuilder() {this._buffer = []; } StringBuilder.prototype = {constructor: StringBuilder,add: function (str) {this._buffer.push(str);},toString: function () {return this._buffer.join('');} };

這種方法將私有變量放入實例對象中,好處是看上去更自然,但是它的私有變量可以從外部讀寫,不是很安全

封裝私有變量:立即執行函數的寫法

另一種做法是使用“立即執行函數”(Immediately-Invoked Function Expression,IIFE),將相關的屬性和方法封裝在一個函數作用域里面,可以達到不暴露私有成員的目的

var module1 = (function () {var _count = 0;var m1 = function () {//...};var m2 = function () {//...};return {m1 : m1, m2 : m2}; })();

使用上面的寫法,外部代碼無法讀取內部的_count變量

console.info(module1._count); //undefined

上面的module1就是 JavaScript 模塊的基本寫法。下面,再對這種寫法進行加工

模塊的放大模式

如果一個模塊很大,必須分成幾個部分,或者一個模塊需要繼承另一個模塊,這時就有必要采用“放大模式”(augmentation)

var module1 = (function (mod){mod.m3 = function () {//...};return mod; })(module1);

上面的代碼為module1模塊添加了一個新方法m3(),然后返回新的module1模塊。在瀏覽器環境中,模塊的各個部分通常都是從網上獲取的,有時無法知道哪個部分會先加載。如果采用上面的寫法,第一個執行的部分有可能加載一個不存在空對象,這時就要采用"寬放大模式"(Loose augmentation)

var module1 = (function (mod) {//...return mod; })(window.module1 || {});

與"放大模式"相比,“寬放大模式”就是“立即執行函數”的參數可以是空對象

輸入全局變量

獨立性是模塊的重要特點,模塊內部最好不與程序的其他部分直接交互。為了在模塊內部調用全局變量,必須顯式地將其他變量輸入模塊

var module1 = (function ($, YAHOO) {//... })(jQuery, YAHOO);

上面的module1模塊需要使用 jQuery 庫和 YUI 庫,就把這兩個庫(其實是兩個模塊)當作參數輸入module1。這樣做除了保證模塊的獨立性,還使得模塊之間的依賴關系變得明顯。立即執行函數還可以起到命名空間的作用

(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對象輸出到全局,對外暴露init和destroy接口,內部方法go、handleEvents、initialize、dieCarouselDie都是外部無法調用的

Object 對象的相關方法

JavaScript 在Object對象上面,提供了很多相關方法,處理面向對象編程的相關操作

Object.getPrototypeOf()

Object.getPrototypeOf方法返回參數對象的原型。這是獲取原型對象的標準方法

var F = function () {}; var f = new F(); Object.getPrototypeOf(f) === F.prototype // true

上面代碼中,實例對象f的原型是F.prototype。下面是幾種特殊對象的原型

// 空對象的原型是 Object.prototype Object.getPrototypeOf({}) === Object.prototype // true // Object.prototype 的原型是 null Object.getPrototypeOf(Object.prototype) === null // true // 函數的原型是 Function.prototype function f() {} Object.getPrototypeOf(f) === Function.prototype // true

Object.setPrototypeOf()

Object.setPrototypeOf方法為參數對象設置原型,返回該參數對象。它接受兩個參數,第一個是現有對象,第二個是原型對象

var a = {}; var b = {x: 1}; Object.setPrototypeOf(a, b); Object.getPrototypeOf(a) === b // true a.x // 1

上面代碼中,Object.setPrototypeOf方法將對象a的原型,設置為對象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命令新建實例對象,其實可以分成兩步。第一步,將一個空對象的原型設為構造函數的prototype屬性(上例是F.prototype);第二步,將構造函數內部的this綁定這個空對象,然后執行構造函數,使得定義在this上面的方法和屬性(上例是this.foo),都轉移到這個空對象上

Object.create()

生成實例對象的常用方法是使用new命令讓構造函數返回一個實例。但是很多時候只能拿到一個實例對象,它可能根本不是由構建函數生成的,那么能不能從一個實例對象生成另一個實例對象呢?JavaScript 提供了Object.create方法,用來滿足這種需求。該方法接受一個對象作為參數,然后以它為原型,返回一個實例對象。該實例完全繼承原型對象的屬性

// 原型對象 var A = {print: function () { console.log('hello'); } }; // 實例對象 var B = Object.create(A); Object.getPrototypeOf(B) === A // true B.print() // hello B.print === A.print // true

上面代碼中,Object.create方法以A對象為原型,生成了B對象。B繼承了A的所有屬性和方法。實際上,Object.create方法可以用下面的代碼代替

if (typeof Object.create !== 'function') {Object.create = function (obj) {function F() {}F.prototype = obj;return new F();}; }

上面代碼表明,Object.create方法的實質是新建一個空的構造函數F,然后讓F.prototype屬性指向參數對象obj,最后返回一個F的實例,從而實現讓該實例繼承obj的屬性。下面三種方式生成的新對象是等價的

var obj1 = Object.create({}); var obj2 = Object.create(Object.prototype); var obj3 = new Object();

如果想要生成一個不繼承任何屬性(比如沒有toString和valueOf方法)的對象,可以將Object.create的參數設為null

var obj = Object.create(null); obj.valueOf() // TypeError: Object [object Object] has no method 'valueOf'

上面代碼中,對象obj的原型是null,它就不具備一些定義在Object.prototype對象上面的屬性,比如valueOf方法。使用Object.create方法的時候,必須提供對象原型,即參數不能為空,或者不是對象,否則會報錯

Object.create() // TypeError: Object prototype may only be an Object or null Object.create(123) // TypeError: Object prototype may only be an Object or null

Object.create方法生成的新對象,動態繼承了原型。在原型上添加或修改任何方法,會立刻反映在新對象之上

var obj1 = { p: 1 }; var obj2 = Object.create(obj1); obj1.p = 2; obj2.p // 2

上面代碼中,修改對象原型obj1會影響到實例對象obj2。除了對象的原型,Object.create方法還可以接受第二個參數。該參數是一個屬性描述對象,它所描述的對象屬性,會添加到實例對象,作為該對象自身的屬性

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方法生成的對象,繼承了它的原型對象的構造函數

function A() {} var a = new A(); var b = Object.create(a); b.constructor === A // true b instanceof A // true

上面代碼中,b對象的原型是a對象,因此繼承了a對象的構造函數A

Object.prototype.isPrototypeOf()

實例對象的isPrototypeOf方法,用來判斷該對象是否為參數對象的原型

var o1 = {}; var o2 = Object.create(o1); var o3 = Object.create(o2); o2.isPrototypeOf(o3) // true o1.isPrototypeOf(o3) // true

上面代碼中,o1和o2都是o3的原型。這表明只要實例對象處在參數對象的原型鏈上,isPrototypeOf方法都返回true

Object.prototype.isPrototypeOf({}) // true Object.prototype.isPrototypeOf([]) // true Object.prototype.isPrototypeOf(/xyz/) // true Object.prototype.isPrototypeOf(Object.create(null)) // false

上面代碼中,由于Object.prototype處于原型鏈的最頂端,所以對各種實例都返回true,只有直接繼承自null的對象除外

Object.prototype.__proto__

實例對象的__proto__屬性(前后各兩個下劃線),返回該對象的原型。該屬性可讀寫

var obj = {}; var p = {}; obj.__proto__ = p; Object.getPrototypeOf(obj) === p // true

上面代碼通過__proto__屬性,將p對象設為obj對象的原型。根據語言標準,__proto__屬性只有瀏覽器才需要部署,其他環境可以沒有這個屬性。它前后的兩根下劃線,表明它本質是一個內部屬性,不應該對使用者暴露;因此,應該盡量少用這個屬性,而是用Object.getPrototypeOf()和Object.setPrototypeOf(),進行原型對象的讀寫操作。原型鏈可以用__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對象和B對象的原型都是proto對象,它們都共享proto對象的print方法。也就是說,A和B的print方法,都是在調用proto對象的print方法

獲取原型對象方法的比較

如前所述,__proto__屬性指向當前對象的原型對象,即構造函數的prototype屬性

var obj = new Object(); obj.__proto__ === Object.prototype // true obj.__proto__ === obj.constructor.prototype // true

上面代碼首先新建了一個對象obj,它的__proto__屬性,指向構造函數(Object或obj.constructor)的prototype屬性;因此,獲取實例對象obj的原型對象,有三種方法

obj.__proto__ obj.constructor.prototype Object.getPrototypeOf(obj)

上面三種方法之中,前兩種都不是很可靠。__proto__屬性只有瀏覽器才需要部署,其他環境可以不部署。而obj.constructor.prototype在手動改變原型對象時,可能會失效

var P = function () {}; var p = new P(); var C = function () {}; C.prototype = p; var c = new C(); c.constructor.prototype === p // false

上面代碼中,構造函數C的原型對象被改成了p,但是實例對象的c.constructor.prototype卻沒有指向p。所以,在改變原型對象時,一般要同時設置constructor屬性

C.prototype = p; C.prototype.constructor = C; var c = new C(); c.constructor.prototype === p // true

因此,推薦使用第三種Object.getPrototypeOf方法,獲取原型對象

Object.getOwnPropertyNames()

Object.getOwnPropertyNames方法返回一個數組,成員是參數對象本身的所有屬性的鍵名,不包含繼承的屬性鍵名

Object.getOwnPropertyNames(Date) // ["length", "name", "prototype", "now", "parse", "UTC"]

上面代碼中,Object.getOwnPropertyNames方法返回Date所有自身的屬性名。對象本身的屬性之中,有的是可以遍歷的(enumerable)有的是不可以遍歷的。Object.getOwnPropertyNames方法返回所有鍵名,不管是否可以遍歷。只獲取那些可以遍歷的屬性,使用Object.keys方法

Object.keys(Date) // []

上面代碼表明,Date對象所有自身的屬性,都是不可以遍歷的

Object.prototype.hasOwnProperty()

對象實例的hasOwnProperty方法返回一個布爾值,用于判斷某個屬性定義在對象自身,還是定義在原型鏈上

Date.hasOwnProperty('length') // true Date.hasOwnProperty('toString') // false

上面代碼表明,Date.length(構造函數Date可以接受多少個參數)是Date自身的屬性,Date.toString是繼承的屬性。另外,hasOwnProperty方法是 JavaScript 之中唯一一個處理對象屬性時,不會遍歷原型鏈的方法

in 運算符和 for...in 循環

in運算符返回一個布爾值,表示一個對象是否具有某個屬性。它不區分該屬性是對象自身的屬性,還是繼承的屬性

'length' in Date // true 'toString' in Date // true

in運算符常用于檢查一個屬性是否存在。獲得對象的所有可遍歷屬性(不管是自身的還是繼承的),可以使用for...in循環

var o1 = { p1: 123 }; var o2 = Object.create(o1, {p2: { value: "abc", enumerable: true } }); for (p in o2) { console.info(p);} // p2 // p1

上面代碼中,對象o2的p2屬性是自身的,p1屬性是繼承的。這兩個屬性都會被for...in循環遍歷。為了在for...in循環中獲得對象自身的屬性,可以采用hasOwnProperty方法判斷一下

for ( var name in object ) {if ( object.hasOwnProperty(name) ) {/* loop code */} }

獲得對象的所有屬性(不管是自身的還是繼承的,也不管是否可枚舉),可以使用下面的函數

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對象的每一級原型對象“自身”的屬性,從而獲取obj對象的“所有”屬性,不管是否可遍歷。下面是一個例子,列出Date對象的所有屬性

inheritedPropertyNames(Date) // ["caller","constructor","toString","UTC",...]

對象的拷貝

如果要拷貝一個對象,需要做到下面兩件事情。

1.確保拷貝后的對象,與原對象具有同樣的原型。

2.確保拷貝后的對象,與原對象具有同樣的實例屬性。

下面就是根據上面兩點,實現的對象拷貝函數

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; }

另一種更簡單的寫法,是利用 ES2017 才引入標準的Object.getOwnPropertyDescriptors方法

function copyObject(orig) {return Object.create(Object.getPrototypeOf(orig),Object.getOwnPropertyDescriptors(orig)); }

嚴格模式

除了正常的運行模式,JavaScript 還有第二種運行模式:嚴格模式(strict mode)。顧名思義,這種模式采用更加嚴格的 JavaScript 語法。同樣的代碼,在正常模式和嚴格模式中,可能會有不一樣的運行結果。一些在正常模式下可以運行的語句,在嚴格模式下將不能運行

設計目的

早期的 JavaScript 語言有很多設計不合理的地方,但是為了兼容以前的代碼,又不能改變老的語法,只能不斷添加新的語法,引導程序員使用新語法。

嚴格模式是從 ES5 進入標準的,主要目的有以下幾個:

1.明確禁止一些不合理、不嚴謹的語法,減少 JavaScript 語言的一些怪異行為

2.增加更多報錯的場合,消除代碼運行的一些不安全之處,保證代碼運行的安全

3.提高編譯器效率,增加運行速度

4.為未來新版本的 JavaScript 語法做好鋪墊

總之,嚴格模式體現了 JavaScript 更合理、更安全、更嚴謹的發展方向

啟用方法

進入嚴格模式的標志,是一行字符串'use strict'

老版本的引擎會把它當作一行普通字符串,加以忽略;新版本的引擎就會進入嚴格模式。嚴格模式可以用于整個腳本,也可以只用于單個函數

整個腳本文件

use strict放在腳本文件的第一行,整個腳本都將以嚴格模式運行;如果這行語句不在第一行就無效,整個腳本會以正常模式運行。(嚴格地說,只要前面不是產生實際運行結果的語句,use strict可以不在第一行,比如直接跟在一個空的分號后面,或者跟在注釋后面)

<script>'use strict';console.log('這是嚴格模式'); </script><script>console.log('這是正常模式'); </script>

上面代碼中,一個網頁文件依次有兩段 JavaScript 代碼。前一個

<script>console.log('這是正常模式');'use strict'; </script>

單個函數

use strict放在函數體的第一行,則整個函數以嚴格模式運行

function strict() {'use strict';return '這是嚴格模式'; }function strict2() {'use strict';function f() {return '這也是嚴格模式';}return f(); }function notStrict() {return '這是正常模式'; }

有時需要把不同腳本合并在一個文件里面。如果一個腳本是嚴格模式,另一個腳本不是,它們的合并就可能出錯。嚴格模式的腳本在前,則合并后的腳本都是嚴格模式;如果正常模式的腳本在前,則合并后的腳本都是正常模式。這兩種情況下,合并后的結果都是不正確的。這時可以考慮把整個腳本文件放在一個立即執行的匿名函數之中

(function () {'use strict';// some code here })();

顯式報錯

嚴格模式使得 JavaScript 的語法變得更嚴格,更多的操作會顯式報錯。其中有些操作,在正常模式下只會默默地失敗,不會報錯

只讀屬性不可寫

嚴格模式下,設置字符串的length屬性,會報錯

'use strict'; 'abc'.length = 5; // TypeError: Cannot assign to read only property 'length' of string 'abc'

上面代碼報錯,因為length是只讀屬性,嚴格模式下不可寫。正常模式下,改變length屬性是無效的,但不會報錯。嚴格模式下,對只讀屬性賦值,或者刪除不可配置(non-configurable)屬性都會報錯

// 對只讀屬性賦值會報錯 'use strict'; Object.defineProperty({}, 'a', {value: 37,writable: false }); obj.a = 123; // TypeError: Cannot assign to read only property 'a' of object #<Object>// 刪除不可配置的屬性會報錯 'use strict'; var obj = Object.defineProperty({}, 'p', {value: 1,configurable: false }); delete obj.p // TypeError: Cannot delete property 'p' of #<Object>

只設置了取值器的屬性不可寫

嚴格模式下,對一個只有取值器(getter)、沒有存值器(setter)的屬性賦值,會報錯

'use strict'; var obj = {get v() { return 1; } }; obj.v = 2; // Uncaught TypeError: Cannot set property v of #<Object> which has only a getter

禁止擴展的對象不可擴展

嚴格模式下,對禁止擴展的對象添加新屬性,會報錯

'use strict'; var obj = {}; Object.preventExtensions(obj); obj.v = 1; // Uncaught TypeError: Cannot add property v, object is not extensible

上面代碼中,obj對象禁止擴展,添加屬性就會報錯

eval、arguments 不可用作標識名

嚴格模式下,使用eval或者arguments作為標識名,將會報錯。下面的語句都會報錯

'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

函數不能有重名的參數

正常模式下,如果函數有多個重名的參數,可以用arguments[i]讀取。嚴格模式下,這屬于語法錯誤

function f(a, a, b) {'use strict';return a + b; } // Uncaught SyntaxError: Duplicate parameter name not allowed in this context

禁止八進制的前綴0表示法

正常模式下,整數的第一位如果是0,表示這是八進制數,比如0100等于十進制的64。嚴格模式禁止這種表示法,整數第一位為0,將報錯

增強的安全措施

嚴格模式增強了安全保護,從語法上防止了一些不小心會出現的錯誤

全局變量顯式聲明

正常模式中,如果一個變量沒有聲明就賦值,默認是全局變量。嚴格模式禁止這種用法,全局變量必須顯式聲明

'use strict'; v = 1; // 報錯,v未聲明 for (i = 0; i < 2; i++) { // 報錯,i 未聲明// ... } function f() {x = 123; } f() // 報錯,未聲明就創建一個全局變量

因此,嚴格模式下,變量都必須先聲明,然后再使用

禁止 this 關鍵字指向全局對象

正常模式下,函數內部的this可能會指向全局對象,嚴格模式禁止這種用法,避免無意間創造全局變量

// 正常模式 function f() {console.log(this === window); } f() // true// 嚴格模式 function f() {'use strict';console.log(this === undefined); } f() // true

上面代碼中,嚴格模式的函數體內部this是undefined,這種限制對于構造函數尤其有用。使用構造函數時,有時忘了加new,這時this不再指向全局對象,而是報錯

function f() {'use strict';this.a = 1; }; f();// 報錯,this 未定義

嚴格模式下,函數直接調用時(不使用new調用),函數內部的this表示undefined(未定義),因此可以用call、apply和bind方法,將任意值綁定在this上面。正常模式下,this指向全局對象,如果綁定的值是非對象,將被自動轉為對象再綁定上去,而null和undefined這兩個無法轉成對象的值,將被忽略

// 正常模式 function fun() { return this; } fun() // window fun.call(2) // Number {2} fun.call(true) // Boolean {true} fun.call(null) // window fun.call(undefined) // window// 嚴格模式 '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

函數內部不得使用fn.caller、fn.arguments,否則會報錯。這意味著不能在函數內部得到調用棧了

function f1() {'use strict';f1.caller; // 報錯f1.arguments; // 報錯 } f1();

禁止使用 arguments.callee、arguments.caller

arguments.callee和arguments.caller是兩個歷史遺留的變量,從來沒有標準化過,現在已經取消了。正常模式下調用它們沒有什么作用,但是不會報錯。嚴格模式明確規定,函數內部使用arguments.callee、arguments.caller將會報錯

'use strict'; var f = function () { return arguments.callee; }; f(); // 報錯

禁止刪除變量

嚴格模式下無法刪除變量,如果使用delete命令刪除一個變量,會報錯。只有對象的屬性,且屬性的描述對象的configurable屬性設置為true,才能被delete命令刪除

'use strict'; var x; delete x; // 語法錯誤var obj = Object.create(null, {x: { value: 1, configurable: true } }); delete obj.x; // 刪除成功

靜態綁定

JavaScript 語言的一個特點,就是允許“動態綁定”,即某些屬性和方法到底屬于哪一個對象,不是在編譯時確定的,而是在運行時(runtime)確定的。嚴格模式對動態綁定做了一些限制;某些情況下,只允許靜態綁定;也就是說,屬性和方法到底歸屬哪個對象,必須在編譯階段就確定。這樣做有利于編譯效率的提高,也使得代碼更容易閱讀,更少出現意外。具體來說,涉及以下幾個方面:

禁止使用 with 語句

嚴格模式下,使用with語句將報錯;因為with語句無法在編譯時就確定,某個屬性到底歸屬哪個對象,從而影響了編譯效果

'use strict'; var v = 1; var obj = {}; with (obj) { v = 2; } // Uncaught SyntaxError: Strict mode code may not include a with statement

創設 eval 作用域

正常模式下,JavaScript 語言有兩種變量作用域(scope):全局作用域和函數作用域。嚴格模式創設了第三種作用域:eval作用域。正常模式下,eval語句的作用域取決于它處于全局作用域還是函數作用域。嚴格模式下,eval語句本身就是一個作用域,不再能夠在其所運行的作用域創設新的變量了,也就是說,eval所生成的變量只能用于eval內部

(function () {'use strict';var x = 2;console.log(eval('var x = 5; x')) // 5console.log(x) // 2 })()

上面代碼中,由于eval語句內部是一個獨立作用域,所以內部的變量x不會泄露到外部。注意,如果希望eval語句也使用嚴格模式,有兩種方式

// 方式一 function f1(str){'use strict';return eval(str); } f1('undeclared_variable = 1'); // 報錯// 方式二 function f2(str){ return eval(str); } f2('"use strict";undeclared_variable = 1') // 報錯

上面兩種寫法,eval內部使用的都是嚴格模式

arguments 不再追蹤參數的變化

變量arguments代表函數的參數。嚴格模式下,函數內部改變參數與arguments的聯系被切斷了,兩者不再存在聯動關系

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); // 嚴格模式為[2, 1]

上面代碼中,改變函數的參數,不會反應到arguments對象上來

向下一個版本的 JavaScript 過渡

JavaScript 語言的下一個版本是 ECMAScript 6,為了平穩過渡,嚴格模式引入了一些 ES6 語法

非函數代碼塊不得聲明函數

ES6 會引入塊級作用域。為了與新版本接軌,ES5 的嚴格模式只允許在全局作用域或函數作用域聲明函數。也就是說,不允許在非函數的代碼塊內聲明函數

'use strict'; if (true) {function f1() { } // 語法錯誤 } for (var i = 0; i < 5; i++) {function f2() { } // 語法錯誤 }

上面代碼在if代碼塊和for代碼塊中聲明了函數,ES5 環境會報錯

注意,如果是 ES6 環境,上面的代碼不會報錯,因為 ES6 允許在代碼塊之中聲明函數

保留字

為了向將來 JavaScript 的新版本過渡,嚴格模式新增了一些保留字(implements、interface、let、package、private、protected、public、static、yield等)。使用這些詞作為變量名將會報錯

function package(protected) { // 語法錯誤'use strict';var implements; // 語法錯誤 }

異步操作

異步操作概述

單線程模型

單線程模型指的是,JavaScript 只在一個線程上運行;也就是說,JavaScript 同時只能執行一個任務,其他任務都必須在后面排隊等待;注意,JavaScript 只在一個線程上運行,不代表 JavaScript 引擎只有一個線程。事實上,JavaScript 引擎有多個線程,單個腳本只能在一個線程上運行(稱為主線程),其他線程都是在后臺配合

JavaScript 之所以采用單線程,而不是多線程,跟歷史有關系;JavaScript 從誕生起就是單線程,原因是不想讓瀏覽器變得太復雜,因為多線程需要共享資源、且有可能修改彼此的運行結果,對于一種網頁腳本語言來說就太復雜了。如果 JavaScript 同時有兩個線程,一個線程在網頁 DOM 節點上添加內容,另一個線程刪除了這個節點,這時瀏覽器應該以哪個線程為準?是不是還要有鎖機制?所以,為了避免復雜性,JavaScript 一開始就是單線程,這已經成了這門語言的核心特征,將來也不會改變

這種模式的好處是實現起來比較簡單,執行環境相對單純;壞處是只要有一個任務耗時很長,后面的任務都必須排隊等著,會拖延整個程序的執行。常見的瀏覽器無響應(假死),往往就是因為某一段 JavaScript 代碼長時間運行(比如死循環),導致整個頁面卡在這個地方,其他任務無法執行。JavaScript 語言本身并不慢,慢的是讀寫外部數據,比如等待 Ajax 請求返回結果;這個時候,如果對方服務器遲遲沒有響應,或者網絡不通暢,就會導致腳本的長時間停滯

如果排隊是因為計算量大,CPU 忙不過來,倒也算了,但是很多時候 CPU 是閑著的,因為 IO 操作(輸入輸出)很慢(比如 Ajax 操作從網絡讀取數據),不得不等著結果出來,再往下執行。JavaScript 語言的設計者意識到,這時 CPU 完全可以不管 IO 操作,掛起處于等待中的任務,先運行排在后面的任務。等到 IO 操作返回了結果,再回過頭,把掛起的任務繼續執行下去。這種機制就是 JavaScript 內部采用的“事件循環”機制(Event Loop)

單線程模型雖然對 JavaScript 構成了很大的限制,但也因此使它具備了其他語言不具備的優勢。如果用得好,JavaScript 程序是不會出現堵塞的,這就是為什么 Node 可以用很少的資源,應付大流量訪問的原因。為了利用多核 CPU 的計算能力,HTML5 提出 Web Worker 標準,允許 JavaScript 腳本創建多個線程,但是子線程完全受主線程控制,且不得操作 DOM。所以,這個新標準并沒有改變 JavaScript 單線程的本質

同步任務和異步任務

程序里面所有的任務,可以分成兩類:同步任務(synchronous)和異步任務(asynchronous)。

同步任務是那些沒有被引擎掛起、在主線程上排隊執行的任務;只有前一個任務執行完畢,才能執行后一個任務。異步任務是那些被引擎放在一邊,不進入主線程、而進入任務隊列的任務。只有引擎認為某個異步任務可以執行了(比如 Ajax 操作從服務器得到了結果),該任務(采用回調函數的形式)才會進入主線程執行。排在異步任務后面的代碼,不用等待異步任務結束會馬上運行,也就是說,異步任務不具有“堵塞”效應。

舉例來說,Ajax 操作可以當作同步任務處理,也可以當作異步任務處理,由開發者決定。如果是同步任務,主線程就等著 Ajax 操作返回結果,再往下執行;如果是異步任務,主線程在發出 Ajax 請求以后,就直接往下執行,等到 Ajax 操作有了結果,主線程再執行對應的回調函數

任務隊列和事件循環

JavaScript 運行時,除了一個正在運行的主線程,引擎還提供一個任務隊列(task queue),里面是各種需要當前程序處理的異步任務。(實際上,根據異步任務的類型,存在多個任務隊列。為了方便理解,這里假設只存在一個隊列)

首先,主線程會去執行所有的同步任務;等到同步任務全部執行完,就會去看任務隊列里面的異步任務;如果滿足條件,那么異步任務就重新進入主線程開始執行,這時它就變成同步任務了。等到執行完,下一個異步任務再進入主線程開始執行。一旦任務隊列清空,程序就結束執行。

異步任務的寫法通常是回調函數。一旦異步任務重新進入主線程,就會執行對應的回調函數。如果一個異步任務沒有回調函數,就不會進入任務隊列,也就是說,不會重新進入主線程,因為沒有用回調函數指定下一步的操作。

JavaScript 引擎怎么知道異步任務有沒有結果,能不能進入主線程呢?答案就是引擎在不停地檢查,一遍又一遍,只要同步任務執行完了,引擎就會去檢查那些掛起來的異步任務,是不是可以進入主線程了。這種循環檢查的機制,就叫做事件循環(Event Loop)。維基百科的定義是:“事件循環是一個程序結構,用于等待和發送消息和事件(a programming construct that waits for and dispatches events or messages in a program)”

異步操作的模式

下面總結一下異步操作的幾種模式

回調函數

回調函數是異步操作最基本的方法。下面是兩個函數f1和f2,編程的意圖是f2必須等到f1執行完成,才能執行

function f1() {// ... } function f2() {// ... } f1(); f2();

上面代碼的問題在于,如果f1是異步操作,f2會立即執行,不會等到f1結束再執行。這時,可以考慮改寫f1,把f2寫成f1的回調函數

function f1(callback) {// ...callback(); } function f2() {// ... } f1(f2);

回調函數的優點是簡單、容易理解和實現,缺點是不利于代碼的閱讀和維護,各個部分之間高度耦合(coupling),使得程序結構混亂、流程難以追蹤(尤其是多個回調函數嵌套的情況),而且每個任務只能指定一個回調函數

事件監聽

另一種思路是采用事件驅動模式。異步任務的執行不取決于代碼的順序,而取決于某個事件是否發生。還是以f1和f2為例;首先,為f1綁定一個事件

f1.on('done', f2);

上面這行代碼的意思是,當f1發生done事件,就執行f2。然后,對f1進行改寫:

function f1() {setTimeout(function () {// ...f1.trigger('done');}, 1000); }

上面代碼中,f1.trigger('done')表示,執行完成后,立即觸發done事件,從而開始執行f2。這種方法的優點是比較容易理解,可以綁定多個事件,每個事件可以指定多個回調函數,而且可以“去耦合”(decoupling),有利于實現模塊化。缺點是整個程序都要變成事件驅動型,運行流程會變得很不清晰。閱讀代碼的時候,很難看出主流程

發布/訂閱

事件完全可以理解成“信號”,如果存在一個“信號中心”,某個任務執行完成,就向信號中心“發布”(publish)一個信號,其他任務可以向信號中心“訂閱”(subscribe)這個信號,從而知道什么時候自己可以開始執行;這就叫做”發布/訂閱模式”(publish-subscribe pattern),又稱“觀察者模式”(observer pattern)。

這個模式有多種實現,下面采用的是 Ben Alman 的 Tiny Pub/Sub,這是 jQuery 的一個插件。

首先,f2向信號中心jQuery訂閱done信號

jQuery.subscribe('done', f2);

然后,f1進行如下改寫

function f1() {setTimeout(function () {// ...jQuery.publish('done');}, 1000); }

上面代碼中,jQuery.publish('done')的意思是,f1執行完成后,向信號中心jQuery發布done信號,從而引發f2的執行;f2完成執行后,可以取消訂閱(unsubscribe)

jQuery.unsubscribe('done', f2);

這種方法的性質與“事件監聽”類似,但是明顯優于后者。因為可以通過查看“消息中心”,了解存在多少信號、每個信號有多少訂閱者,從而監控程序的運行

異步操作的流程控制

如果有多個異步操作,就存在一個流程控制的問題:如何確定異步操作執行的順序,以及如何保證遵守這種順序

function async(arg, callback) {console.log('參數為 ' + arg +' , 1秒后返回結果');setTimeout(function () { callback(arg * 2); }, 1000); }

上面代碼的async函數是一個異步任務,非常耗時,每次執行需要1秒才能完成,然后再調用回調函數;如果有六個這樣的異步任務,需要全部完成后,才能執行最后的final函數。請問應該如何安排操作流程?

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);});});});}); }); // 參數為 1 , 1秒后返回結果 // 參數為 2 , 1秒后返回結果 // 參數為 3 , 1秒后返回結果 // 參數為 4 , 1秒后返回結果 // 參數為 5 , 1秒后返回結果 // 參數為 6 , 1秒后返回結果 // 完成: 12

上面代碼中,六個回調函數的嵌套,不僅寫起來麻煩,容易出錯,而且難以維護

串行執行

我們可以編寫一個流程控制函數,讓它來控制異步任務,一個任務完成以后,再執行另一個。這就叫串行執行

var items = [ 1, 2, 3, 4, 5, 6 ]; var results = []; function async(arg, callback) {console.log('參數為 ' + arg +' , 1秒后返回結果');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());

上面代碼中,函數series就是串行函數,它會依次執行異步任務,所有任務都完成后,才會執行final函數。items數組保存每一個異步任務的參數,results數組保存每一個異步任務的運行結果。注意,上面的寫法需要六秒,才能完成整個腳本

并行執行

流程控制函數也可以是并行執行,即所有異步任務同時執行,等到全部完成以后,才執行final函數

var items = [ 1, 2, 3, 4, 5, 6 ]; var results = []; function async(arg, callback) {console.log('參數為 ' + arg +' , 1秒后返回結果');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方法會同時發起六個異步任務,等到它們全部完成以后,才會執行final函數。相比而言,上面的寫法只要一秒就能完成整個腳本;這就是說,并行執行的效率較高,比起串行執行一次只能執行一個任務,較為節約時間;但是問題在于如果并行的任務較多,很容易耗盡系統資源,拖慢運行速度。因此有了第三種流程控制方式

并行與串行的結合

所謂并行與串行的結合,就是設置一個門檻,每次最多只能并行執行n個異步任務,這樣就避免了過分占用系統資源

var items = [ 1, 2, 3, 4, 5, 6 ]; var results = []; var running = 0; var limit = 2; function async(arg, callback) {console.log('參數為 ' + arg +' , 1秒后返回結果');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();

上面代碼中,最多只能同時運行兩個異步任務。變量running記錄當前正在運行的任務數,只要低于門檻值,就再啟動一個新的任務,如果等于0,就表示所有任務都執行完了,這時就執行final函數。

這段代碼需要三秒完成整個腳本,處在串行執行和并行執行之間。通過調節limit變量,達到效率和資源的最佳平衡

定時器

JavaScript 提供定時執行代碼的功能,叫做定時器(timer),主要由setTimeout()和setInterval()這兩個函數來完成。它們向任務隊列添加定時任務

setTimeout()

setTimeout函數用來指定某個函數或某段代碼,在多少毫秒之后執行。它返回一個整數,表示定時器的編號,以后可以用來取消這個定時器

var timerId = setTimeout(func|code, delay);

上面代碼中,setTimeout函數接受兩個參數,第一個參數func|code是將要推遲執行的函數名或者一段代碼,第二個參數delay是推遲執行的毫秒數

console.log(1); setTimeout('console.log(2)',1000); console.log(3); // 1 // 3 // 2

上面代碼會先輸出1和3,然后等待1000毫秒再輸出2。注意,console.log(2)必須以字符串的形式,作為setTimeout的參數。如果推遲執行的是函數,就直接將函數名,作為setTimeout的參數

function f() { console.log(2); } setTimeout(f, 1000);

setTimeout的第二個參數如果省略,則默認為0

setTimeout(f) // 等同于 setTimeout(f, 0)

除了前兩個參數,setTimeout還允許更多的參數。它們將依次傳入推遲執行的函數(回調函數)

setTimeout(function (a,b) {console.log(a + b); }, 1000, 1, 1);

上面代碼中,setTimeout共有4個參數。最后兩個參數,將在1000毫秒之后回調函數執行時作為回調函數的參數。還有一個需要注意的地方,如果回調函數是對象的方法,那么setTimeout使得方法內部的this關鍵字指向全局環境,而不是定義時所在的那個對象

var x = 1; var obj = {x: 2,y: function () { console.log(this.x); } }; setTimeout(obj.y, 1000) // 1

上面代碼輸出的是1,而不是2。因為當obj.y在1000毫秒后運行時,this所指向的已經不是obj了,而是全局環境。為了防止出現這個問題,一種解決方法是將obj.y放入一個函數

var x = 1; var obj = {x: 2,y: function () { console.log(this.x); } }; setTimeout(function () { obj.y(); }, 1000); // 2

上面代碼中,obj.y放在一個匿名函數之中,這使得obj.y在obj的作用域執行,而不是在全局作用域內執行,所以能夠顯示正確的值;另一種解決方法是,使用bind方法,將obj.y這個方法綁定在obj上面

var x = 1; var obj = {x: 2,y: function () { console.log(this.x); } }; setTimeout(obj.y.bind(obj), 1000) // 2

setInterval()

setInterval函數的用法與setTimeout完全一致,區別僅僅在于setInterval指定某個任務每隔一段時間就執行一次,也就是無限次的定時執行

var i = 1 var timer = setInterval(function() { console.log(2); }, 1000)

上面代碼中,每隔1000毫秒就輸出一個2,會無限運行下去,直到關閉當前窗口;與setTimeout一樣,除了前兩個參數,setInterval方法還可以接受更多的參數,它們會傳入回調函數。下面是一個通過setInterval方法實現網頁動畫的例子

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毫秒,設置一次div元素的透明度,直至其完全透明為止。setInterval的一個常見用途是實現輪詢。下面是一個輪詢 URL 的 Hash 值是否發生變化的例子

var hash = window.location.hash; var hashWatcher = setInterval(function() {if (window.location.hash != hash) {updatePage();} }, 1000);

setInterval指定的是“開始執行”之間的間隔,并不考慮每次任務執行本身所消耗的時間。因此實際上,兩次執行之間的間隔會小于指定的時間。比如,setInterval指定每 100ms 執行一次,每次執行需要 5ms,那么第一次執行結束后95毫秒,第二次執行就會開始。如果某次執行耗時特別長,比如需要105毫秒,那么它結束后,下一次執行就會立即開始。為了確保兩次執行之間有固定的間隔,可以不用setInterval,而是每次執行結束后,使用setTimeout指定下一次執行的具體時間

var i = 1; var timer = setTimeout(function f() {// ...timer = setTimeout(f, 2000); }, 2000);

上面代碼可以確保,下一次執行總是在本次執行結束之后的2000毫秒開始

clearTimeout(),clearInterval()

setTimeout和setInterval函數,都返回一個整數值,表示計數器編號。將該整數傳入clearTimeout和clearInterval函數,就可以取消對應的定時器

var id1 = setTimeout(f, 1000); var id2 = setInterval(f, 1000); clearTimeout(id1); clearInterval(id2);

上面代碼中,回調函數f不會再執行了,因為兩個定時器都被取消了。setTimeout和setInterval返回的整數值是連續的,也就是說,第二個setTimeout方法返回的整數值,將比第一個的整數值大1

function f() {} setTimeout(f, 1000) // 10 setTimeout(f, 1000) // 11 setTimeout(f, 1000) // 12

上面代碼中,連續調用三次setTimeout,返回值都比上一次大了1。利用這一點,可以寫一個函數,取消當前所有的setTimeout定時器

(function() { // 每輪事件循環檢查一次 var gid = setInterval(clearAllTimeouts, 0);function clearAllTimeouts() {var id = setTimeout(function() {}, 0);while (id > 0) {if (id !== gid) { clearTimeout(id); }id--;}} })();

上面代碼中,先調用setTimeout,得到一個計算器編號,然后把編號比它小的計數器全部取消

實例:debounce 函數

有時,我們不希望回調函數被頻繁調用。比如,用戶填入網頁輸入框的內容,希望通過 Ajax 方法傳回服務器,jQuery 的寫法如下:

$('textarea').on('keydown', ajaxAction);

這樣寫有一個很大的缺點,就是如果用戶連續擊鍵,就會連續觸發keydown事件,造成大量的 Ajax 通信。這是不必要的,而且很可能產生性能問題。正確的做法應該是,設置一個門檻值,表示兩次 Ajax 通信的最小間隔時間。如果在間隔時間內,發生新的keydown事件,則不觸發 Ajax 通信,并且重新開始計時。如果過了指定時間,沒有發生新的keydown事件,再將數據發送出去。

這種做法叫做 debounce(防抖動)。假定兩次 Ajax 通信的間隔不得小于2500毫秒,上面的代碼可以改寫成下面這樣

$('textarea').on('keydown', debounce(ajaxAction, 2500)); function debounce(fn, delay){var timer = null; // 聲明計時器return function() {var context = this;var args = arguments;clearTimeout(timer);timer = setTimeout(function () { fn.apply(context, args); }, delay);}; }

上面代碼中,只要在2500毫秒之內,用戶再次擊鍵,就會取消上一次的定時器,然后再新建一個定時器。這樣就保證了回調函數之間的調用間隔,至少是2500毫秒

運行機制

setTimeout和setInterval的運行機制,是將指定的代碼移出本輪事件循環,等到下一輪事件循環,再檢查是否到了指定時間。如果到了,就執行對應的代碼;如果不到,就繼續等待。這意味著,setTimeout和setInterval指定的回調函數,必須等到本輪事件循環的所有同步任務都執行完,才會開始執行。由于前面的任務到底需要多少時間執行完,是不確定的,所以沒有辦法保證,setTimeout和setInterval指定的任務,一定會按照預定時間執行

setTimeout(someTask, 100); veryLongTask();

上面代碼的setTimeout,指定100毫秒以后運行一個任務。但是,如果后面的veryLongTask函數(同步任務)運行時間非常長,過了100毫秒還無法結束,那么被推遲運行的someTask就只有等著,等到veryLongTask運行結束,才輪到它執行。再看一個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的作用是將代碼推遲到指定時間執行,如果指定時間為0,即setTimeout(f, 0),那么會立刻執行嗎?

答案是不會。因為上一節說過,必須要等到當前腳本的同步任務,全部處理完以后,才會執行setTimeout指定的回調函數f。也就是說,setTimeout(f, 0)會在下一輪事件循環一開始就執行

setTimeout(function () { console.log(1); }, 0); console.log(2); // 2 // 1

上面代碼先輸出2,再輸出1。因為2是同步任務,在本輪事件循環執行,而1是下一輪事件循環執行。總之,setTimeout(f, 0)這種寫法的目的是,盡可能早地執行f,但是并不能保證立刻就執行f。實際上,setTimeout(f, 0)不會真的在0毫秒之后運行,不同的瀏覽器有不同的實現。以 Edge 瀏覽器為例,會等到4毫秒之后運行。如果電腦正在使用電池供電,會等到16毫秒之后運行;如果網頁不在當前 Tab 頁,會推遲到1000毫秒(1秒)之后運行。這樣是為了節省系統資源

應用

setTimeout(f, 0)有幾個非常重要的用途。它的一大應用是,可以調整事件的發生順序。比如,網頁開發中,某個事件先發生在子元素,然后冒泡到父元素,即子元素的事件回調函數,會早于父元素的事件回調函數觸發。如果,想讓父元素的事件回調函數先發生,就要用到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>

上面代碼在點擊按鈕后,先觸發回調函數A,然后觸發函數C。函數A中,setTimeout將函數B推遲到下一輪事件循環執行,這樣就起到了,先觸發父元素的回調函數C的目的了。

另一個應用是,用戶自定義的回調函數,通常在瀏覽器的默認動作之前觸發。比如,用戶在輸入框輸入文本,keypress事件會在瀏覽器接收文本之前觸發。因此,下面的回調函數是達不到目的的

<input type="text" id="input-box"> document.getElementById('input-box').onkeypress = function (event) { this.value = this.value.toUpperCase(); }

上面代碼想在用戶每次輸入文本后,立即將字符轉為大寫。但是實際上,它只能將本次輸入前的字符轉為大寫,因為瀏覽器此時還沒接收到新的文本,所以this.value取不到最新輸入的那個字符。只有用setTimeout改寫,上面的代碼才能發揮作用

document.getElementById('input-box').onkeypress = function() {var self = this;setTimeout(function() { self.value = self.value.toUpperCase(); }, 0); }

由于setTimeout(f, 0)實際上意味著,將任務放到瀏覽器最早可得的空閑時段執行,所以那些計算量大、耗時長的任務,常常會被放到幾個小部分,分別放到setTimeout(f, 0)里面執行

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);

上面代碼有兩種寫法,都是改變一個網頁元素的背景色。寫法一會造成瀏覽器“堵塞”,因為 JavaScript 執行速度遠高于 DOM,會造成大量 DOM 操作“堆積”,而寫法二就不會,這就是setTimeout(f, 0)的好處。

另一個使用這種技巧的例子是代碼高亮的處理。如果代碼塊很大,一次性處理,可能會對性能造成很大的壓力,那么將其分成一個個小塊,一次處理一塊,比如寫成setTimeout(highlightNext, 50)的樣子,性能壓力就會減輕。

Promise 對象

概述

Promise 對象是 JavaScript 的異步操作解決方案,為異步操作提供統一接口。它起到代理作用(proxy),充當異步操作與回調函數之間的中介,使得異步操作具備同步操作的接口。Promise 可以讓異步操作寫起來,就像在寫同步操作的流程,而不必一層層地嵌套回調函數。

首先,Promise 是一個對象,也是一個構造函數

function f1(resolve, reject) {// 異步代碼... } var p1 = new Promise(f1);

上面代碼中,Promise構造函數接受一個回調函數f1作為參數,f1里面是異步操作的代碼。然后,返回的p1就是一個 Promise 實例。Promise 的設計思想是,所有異步任務都返回一個 Promise 實例。Promise 實例有一個then方法,用來指定下一步的回調函數

var p1 = new Promise(f1); p1.then(f2);

上面代碼中,f1的異步操作執行完成,就會執行f2。傳統的寫法可能需要把f2作為回調函數傳入f1,比如寫成f1(f2),異步操作完成后,在f1內部調用f2。Promise 使得f1和f2變成了鏈式寫法。不僅改善了可讀性,而且對于多層嵌套的回調函數尤其方便

// 傳統寫法 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 以后,程序流程變得非常清楚,十分易讀。總的來說,傳統的回調函數寫法使得代碼混成一團,變得橫向發展而不是向下發展。Promise 就是解決這個問題,使得異步流程可以寫成同步流程。Promise 原本只是社區提出的一個構想,一些函數庫率先實現了這個功能。ECMAScript 6 將其寫入語言標準,目前 JavaScript 原生支持 Promise 對象

Promise 對象的狀態

Promise 對象通過自身的狀態,來控制異步操作。Promise 實例具有三種狀態:異步操作未完成(pending)/異步操作成功(fulfilled)/異步操作失敗(rejected);上面三種狀態里面,fulfilled和rejected合在一起稱為resolved(已定型)。

這三種的狀態的變化途徑只有兩種:從“未完成”到“成功” / 從“未完成”到“失敗”。一旦狀態發生變化就凝固了,不會再有新的狀態變化。這也是 Promise 這個名字的由來,它的英語意思是“承諾”,一旦承諾成效,就不能再改變了。這也意味著,Promise 實例的狀態變化只可能發生一次。因此,Promise 的最終結果只有兩種:

異步操作成功,Promise 實例傳回一個值(value),狀態變為fulfilled

異步操作失敗,Promise 實例拋出一個錯誤(error),狀態變為rejected

Promise 構造函數

JavaScript 提供原生的Promise構造函數,用來生成 Promise 實例

var promise = new Promise(function (resolve, reject) {// ...if (/* 異步操作成功 */){resolve(value);} else { /* 異步操作失敗 */reject(new Error());} });

上面代碼中,Promise構造函數接受一個函數作為參數,該函數的兩個參數分別是resolve和reject。它們是兩個函數,由 JavaScript 引擎提供,不用自己實現。resolve函數的作用是,將Promise實例的狀態從“未完成”變為“成功”(即從pending變為fulfilled),在異步操作成功時調用,并將異步操作的結果作為參數傳遞出去。reject函數的作用是,將Promise實例的狀態從“未完成”變為“失敗”(即從pending變為rejected),在異步操作失敗時調用,并將異步操作報出的錯誤,作為參數傳遞出去

function timeout(ms) {return new Promise((resolve, reject) => { setTimeout(resolve, ms, 'done'); }); } timeout(100)

上面代碼中,timeout(100)返回一個 Promise 實例。100毫秒以后,該實例的狀態會變為fulfilled

Promise.prototype.then()

Promise 實例的then方法,用來添加回調函數。then方法可以接受兩個回調函數,第一個是異步操作成功時(變為fulfilled狀態)的回調函數,第二個是異步操作失敗(變為rejected)時的回調函數(該參數可以省略)。一旦狀態改變,就調用相應的回調函數

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 實例,它們的then方法綁定兩個回調函數:成功時的回調函數console.log,失敗時的回調函數console.error(可以省略)。p1的狀態變為成功,p2的狀態變為失敗,對應的回調函數會收到異步操作傳回的值,然后在控制臺輸出。then方法可以鏈式使用

p1.then(step1).then(step2).then(step3).then( console.log, console.error);

上面代碼中,p1后面有四個then,意味著依次有四個回調函數。只要前一步的狀態變為fulfilled,就會依次執行緊跟在后面的回調函數。最后一個then方法,回調函數是console.log和console.error,用法上有一點重要的區別;console.log只顯示step3的返回值,而console.error可以顯示p1、step1、step2、step3之中任意一個發生的錯誤。舉例來說,如果step1的狀態變為rejected,那么step2和step3都不會執行了(因為它們是resolved的回調函數)。Promise 開始尋找,接下來第一個為rejected的回調函數,在上面代碼中是console.error。這就是說,Promise 對象的報錯具有傳遞性

then() 用法辨析

Promise 的用法,簡單說就是一句話:使用then方法添加回調函數。但是,不同的寫法有一些細微的差別,請看下面四種寫法:

// 寫法一 f1().then(function () { return f2(); }); // 寫法二 f1().then(function () { f2(); }); // 寫法三 f1().then(f2()); // 寫法四 f1().then(f2);

為了便于講解,下面這四種寫法都再用then方法接一個回調函數f3。寫法一的f3回調函數的參數,是f2函數的運行結果

f1().then(function () { return f2(); }).then(f3);

寫法二的f3回調函數的參數是undefined

f1().then(function () { f2(); return; }).then(f3);

寫法三的f3回調函數的參數,是f2函數返回的函數的運行結果

f1().then(f2()).then(f3);

寫法四與寫法一只有一個差別,那就是f2會接收到f1()返回的結果

f1().then(f2).then(f3);

實例:圖片加載

下面是使用 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函數用法如下

preloadImage('https://example.com/my.jpg').then(function (e) { document.body.append(e.target) }).then(function () { console.log('加載成功') })

小結

Promise 的優點在于,讓回調函數變成了規范的鏈式寫法,程序流程可以看得很清楚。它有一整套接口,可以實現許多強大的功能,比如同時執行多個異步操作,等到它們的狀態都改變以后,再執行一個回調函數;再比如,為多個回調函數中拋出的錯誤,統一指定處理方法等等。而且,Promise 還有一個傳統寫法沒有的好處:它的狀態一旦改變,無論何時查詢,都能得到這個狀態。這意味著,無論何時為 Promise 實例添加回調函數,該函數都能正確執行。所以,你不用擔心是否錯過了某個事件或信號。如果是傳統寫法,通過監聽事件來執行回調函數,一旦錯過了事件,再添加回調函數是不會執行的。

Promise 的缺點是,編寫的難度比傳統寫法高,而且閱讀代碼也不是一眼可以看懂。你只會看到一堆then,必須自己在then的回調函數里面理清邏輯

微任務

Promise 的回調函數屬于異步任務,會在同步任務之后執行

new Promise(function (resolve, reject) { resolve(1); }).then(console.log); console.log(2); // 2 // 1

上面代碼會先輸出2,再輸出1。因為console.log(2)是同步任務,而then的回調函數屬于異步任務,一定晚于同步任務執行;但是,Promise 的回調函數不是正常的異步任務,而是微任務(microtask)。它們的區別在于,正常任務追加到下一輪事件循環,微任務追加到本輪事件循環。這意味著,微任務的執行時間一定早于正常任務

setTimeout(function() { console.log(1); }, 0); new Promise(function (resolve, reject) { resolve(2); }).then(console.log); console.log(3); // 3 // 2 // 1

上面代碼的輸出結果是321。這說明then的回調函數的執行時間,早于setTimeout(fn, 0)。因為then是本輪事件循環執行,setTimeout(fn, 0)在下一輪事件循環開始時執行

DOM

DOM 概述

DOM

DOM 是 JavaScript 操作網頁的接口,全稱為“文檔對象模型”(Document Object Model)。它的作用是將網頁轉為一個 JavaScript 對象,從而可以用腳本進行各種操作(比如增刪內容)。瀏覽器會根據 DOM 模型,將結構化文檔(比如 HTML 和 XML)解析成一系列的節點,再由這些節點組成一個樹狀結構(DOM Tree)。所有的節點和最終的樹狀結構,都有規范的對外接口

DOM 只是一個接口規范,可以用各種語言實現。所以嚴格地說,DOM 不是 JavaScript 語法的一部分,但是 DOM 操作是 JavaScript 最常見的任務,離開了 DOM,JavaScript 就無法控制網頁。另一方面,JavaScript 也是最常用于 DOM 操作的語言

節點

DOM 的最小組成單位叫做節點(node)。文檔的樹形結構(DOM 樹),就是由各種不同類型的節點組成。每個節點可以看作是文檔樹的一片葉子

節點的類型有七種。

1.Document:整個文檔樹的頂層節點

2.DocumentType:doctype標簽(比如)

3.Element:網頁的各種HTML標簽(比如

、等)

4.Attribute:網頁元素的屬性(比如class="right")

5.Text:標簽之間或標簽包含的文本

6.Comment:注釋

7.DocumentFragment:文檔的片段

瀏覽器提供一個原生的節點對象Node,上面這七種節點都繼承了Node,因此具有一些共同的屬性和方法

節點樹

一個文檔的所有節點,按照所在的層級,可以抽象成一種樹狀結構;這種樹狀結構就是 DOM 樹。它有一個頂層節點,下一層都是頂層節點的子節點,然后子節點又有自己的子節點,就這樣層層衍生出一個金字塔結構,倒過來就像一棵樹。瀏覽器原生提供document節點,代表整個文檔

document // 整個文檔樹

文檔的第一層只有一個節點,就是 HTML 網頁的第一個標簽,它構成了樹結構的根節點(root node),其他 HTML 標簽節點都是它的下級節點;除了根節點,其他節點都有三種層級關系:

1.父節點關系(parentNode):直接的那個上級節點

2.子節點關系(childNodes):直接的下級節點
3.同級節點關系(sibling):擁有同一個父節點的節點

DOM 提供操作接口,用來獲取這三種關系的節點。比如,子節點接口包括firstChild(第一個子節點)和lastChild(最后一個子節點)等屬性,同級節點接口包括nextSibling(緊鄰在后的那個同級節點)和previousSibling(緊鄰在前的那個同級節點)屬性

Node 接口

所有 DOM 節點對象都繼承了 Node 接口,擁有一些共同的屬性和方法。這是 DOM 操作的基礎

屬性

Node.prototype.nodeType

nodeType屬性返回一個整數值,表示節點的類型

document.nodeType // 9

上面代碼中,文檔節點的類型值為9。Node 對象定義了幾個常量,對應這些類型值

document.nodeType === Node.DOCUMENT_NODE // true

上面代碼中,文檔節點的nodeType屬性等于常量Node.DOCUMENT_NODE;不同節點的nodeType屬性值和對應的常量如下:

1.文檔節點(document):9,對應常量Node.DOCUMENT_NODE

2.元素節點(element):1,對應常量Node.ELEMENT_NODE

3.屬性節點(attr):2,對應常量Node.ATTRIBUTE_NODE

4.文本節點(text):3,對應常量Node.TEXT_NODE

5.文檔片斷節點(DocumentFragment):11,對應常量Node.DOCUMENT_FRAGMENT_NODE

6.文檔類型節點(DocumentType):10,對應常量Node.DOCUMENT_TYPE_NODE

7.注釋節點(Comment):8,對應常量Node.COMMENT_NODE

確定節點類型時,使用nodeType屬性是常用方法

var node = document.documentElement.firstChild; if (node.nodeType === Node.ELEMENT_NODE) { console.log('該節點是元素節點'); }

Node.prototype.nodeName

nodeName屬性返回節點的名稱

<div id="d1">hello world</div> <script>var div = document.getElementById('d1');div.nodeName // "DIV" </script>

上面代碼中,元素節點

的nodeName屬性就是大寫的標簽名DIV;不同節點的nodeName屬性值如下:

1.文檔節點(document):#document

2.元素節點(element):大寫的標簽名

3.屬性節點(attr):屬性的名稱

4.文本節點(text):#text

5.文檔片斷節點(DocumentFragment):#document-fragment

6.文檔類型節點(DocumentType):文檔的類型

7.注釋節點(Comment):#comment

Node.prototype.nodeValue

nodeValue屬性返回一個字符串,表示當前節點本身的文本值,該屬性可讀寫;只有文本節點(text)、注釋節點(comment)和屬性節點(attr)有文本值,因此這三類節點的nodeValue可以返回結果,其他類型的節點一律返回null。同樣的,也只有這三類節點可以設置nodeValue屬性的值,其他類型的節點設置無效

<div id="d1">hello world</div> <script>var div = document.getElementById('d1');div.nodeValue // nulldiv.firstChild.nodeValue // "hello world" </script>

上面代碼中,div是元素節點,nodeValue屬性返回null。div.firstChild是文本節點,所以可以返回文本值

Node.prototype.textContent

textContent屬性返回當前節點和它的所有后代節點的文本內容

<div id="divA">This is <span>some</span> text</div> document.getElementById('divA').textContent // This is some text

textContent屬性自動忽略當前節點內部的 HTML 標簽,返回所有文本內容;該屬性是可讀寫的,設置該屬性的值,會用一個新的文本節點,替換所有原來的子節點。它還有一個好處,就是自動對 HTML 標簽轉義。這很適合用于用戶提供的內容

document.getElementById('foo').textContent = '<p>GoodBye!</p>'

上面代碼在插入文本時,會將

標簽解釋為文本,而不會當作標簽處理。

對于文本節點(text)、注釋節點(comment)和屬性節點(attr),textContent屬性的值與nodeValue屬性相同。對于其他類型的節點,該屬性會將每個子節點(不包括注釋節點)的內容連接在一起返回。如果一個節點沒有子節點,則返回空字符串。文檔節點(document)和文檔類型節點(doctype)的textContent屬性為null。如果要讀取整個文檔的內容,可以使用document.documentElement.textContent

Node.prototype.baseURI

baseURI屬性返回一個字符串,表示當前網頁的絕對路徑。瀏覽器根據這個屬性,計算網頁上的相對路徑的 URL。該屬性為只讀

document.baseURI

如果無法讀到網頁的 URL,baseURI屬性返回null;該屬性的值一般由當前網址的 URL(即window.location屬性)決定,但是可以使用 HTML 的標簽,改變該屬性的值

<base href="http://www.example.com/page.html">

設置了以后,baseURI屬性就返回標簽設置的值

Node.prototype.ownerDocument

Node.ownerDocument屬性返回當前節點所在的頂層文檔對象,即document對象

var d = p.ownerDocument; d === document // true

document對象本身的ownerDocument屬性,返回null

Node.prototype.nextSibling

Node.nextSibling屬性返回緊跟在當前節點后面的第一個同級節點;如果當前節點后面沒有同級節點,則返回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

注意,該屬性還包括文本節點和注釋節點()。因此如果當前節點后面有空格,該屬性會返回一個文本節點,內容為空格;nextSibling屬性可以用來遍歷所有子節點

var el = document.getElementById('div1').firstChild; while (el !== null) {console.log(el.nodeName);el = el.nextSibling; }

上面代碼遍歷div1節點的所有子節點

Node.prototype.previousSibling

previousSibling屬性返回當前節點前面的、距離最近的一個同級節點。如果當前節點前面沒有同級節點,則返回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前面的同級節點d1。注意,該屬性還包括文本節點和注釋節點。因此如果當前節點前面有空格,該屬性會返回一個文本節點,內容為空格

Node.prototype.parentNode

parentNode屬性返回當前節點的父節點。對于一個節點來說,它的父節點只可能是三種類型:元素節點(element)、文檔節點(document)和文檔片段節點(documentfragment)

if (node.parentNode) { node.parentNode.removeChild(node); }

上面代碼中,通過node.parentNode屬性將node節點從文檔里面移除;文檔節點(document)和文檔片段節點(documentfragment)的父節點都是null。另外,對于那些生成后還沒插入 DOM 樹的節點,父節點也是null

Node.prototype.parentElement

parentElement屬性返回當前節點的父元素節點;如果當前節點沒有父節點,或者父節點類型不是元素節點,則返回null

if (node.parentElement) { node.parentElement.style.color = 'red'; }

上面代碼中,父元素節點的樣式設定了紅色;由于父節點只可能是三種類型:元素節點、文檔節點(document)和文檔片段節點(documentfragment);parentElement屬性相當于把后兩種父節點都排除了

Node.prototype.firstChild,Node.prototype.lastChild

firstChild屬性返回當前節點的第一個子節點,如果當前節點沒有子節點,則返回null

<p id="p1"><span>First span</span></p> var p1 = document.getElementById('p1'); p1.firstChild.nodeName // "SPAN"

注意,firstChild返回的除了元素節點,還可能是文本節點或注釋節點

<p id="p1"><span>First span</span></p> var p1 = document.getElementById('p1'); p1.firstChild.nodeName // "#text"

lastChild屬性返回當前節點的最后一個子節點,如果當前節點沒有子節點,則返回null。用法與firstChild屬性相同

Node.prototype.childNodes

childNodes屬性返回一個類似數組的對象(NodeList集合),成員包括當前節點的所有子節點

var children = document.querySelector('ul').childNodes;

上面代碼中,children就是ul元素的所有子節點;使用該屬性,可以遍歷某個節點的所有子節點

var div = document.getElementById('div1'); var children = div.childNodes; for (var i = 0; i < children.length; i++) {// ... }

文檔節點(document)就有兩個子節點:文檔類型節點(docType)和 HTML 根元素節點

var children = document.childNodes; for (var i = 0; i < children.length; i++) { console.log(children[i].nodeType); } // 10 // 1

上面代碼中,文檔節點的第一個子節點的類型是10(即文檔類型節點),第二個子節點的類型是1(即元素節點)

注意,除了元素節點,childNodes屬性的返回值還包括文本節點和注釋節點;如果當前節點不包括任何子節點,則返回一個空的NodeList集合。由于NodeList對象是一個動態集合,一旦子節點發生變化,立刻會反映在返回結果之中

Node.prototype.isConnected

isConnected屬性返回一個布爾值,表示當前節點是否在文檔之中

var test = document.createElement('p'); test.isConnected // false document.body.appendChild(test); test.isConnected // true

上面代碼中,test節點是腳本生成的節點,沒有插入文檔之前,isConnected屬性返回false,插入之后返回true

方法

Node.prototype.appendChild()

appendChild()方法接受一個節點對象作為參數,將其作為最后一個子節點,插入當前節點。該方法的返回值就是插入文檔的子節點

var p = document.createElement('p'); document.body.appendChild(p);

上面代碼新建一個

節點,將其插入document.body的尾部;如果參數節點是 DOM 已經存在的節點,appendChild()方法會將其從原來的位置,移動到新位置

var div = document.getElementById('myDiv'); document.body.appendChild(div);

上面代碼中,插入的是一個已經存在的節點myDiv,結果就是該節點會從原來的位置,移動到document.body的尾部;如果appendChild()方法的參數是DocumentFragment節點,那么插入的是DocumentFragment的所有子節點,而不是DocumentFragment節點本身。返回值是一個空的DocumentFragment節點

Node.prototype.hasChildNodes()

hasChildNodes方法返回一個布爾值,表示當前節點是否有子節點

var foo = document.getElementById('foo'); if (foo.hasChildNodes()) { foo.removeChild(foo.childNodes[0]); }

上面代碼表示,如果foo節點有子節點,就移除第一個子節點;注意,子節點包括所有類型的節點,并不僅僅是元素節點。哪怕節點只包含一個空格,hasChildNodes方法也會返回true。判斷一個節點有沒有子節點,有許多種方法,下面是其中的三種:

1.node.hasChildNodes()

2.node.firstChild !== null

3.node.childNodes && node.childNodes.length > 0

hasChildNodes方法結合firstChild屬性和nextSibling屬性,可以遍歷當前節點的所有后代節點

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函數的第一個參數是某個指定的節點,第二個參數是回調函數。這個回調函數會依次作用于指定節點,以及指定節點的所有后代節點

Node.prototype.cloneNode()

cloneNode方法用于克隆一個節點。它接受一個布爾值作為參數,表示是否同時克隆子節點。它的返回值是一個克隆出來的新節點

var cloneUL = document.querySelector('ul').cloneNode(true);

該方法有一些使用注意點:

1.克隆一個節點,會拷貝該節點的所有屬性,但是會喪失addEventListener方法和on-屬性(即node.onclick = fn),添加在這個節點上的事件回調函數

2.該方法返回的節點不在文檔之中,即沒有任何父節點,必須使用諸如Node.appendChild這樣的方法添加到文檔之中。

3.克隆一個節點之后,DOM 有可能出現兩個有相同id屬性(即id="xxx")的網頁元素,這時應該修改其中一個元素的id屬性。如果原節點有name屬性,可能也需要修改

Node.prototype.insertBefore()

insertBefore方法用于將某個節點插入父節點內部的指定位置

var insertedNode = parentNode.insertBefore(newNode, referenceNode);

insertBefore方法接受兩個參數,第一個參數是所要插入的節點newNode,第二個參數是父節點parentNode內部的一個子節點referenceNode。newNode將插在referenceNode這個子節點的前面。返回值是插入的新節點newNode

var p = document.createElement('p'); document.body.insertBefore(p, document.body.firstChild);

上面代碼中,新建一個

節點,插在document.body.firstChild的前面,也就是成為document.body的第一個子節點。如果insertBefore方法的第二個參數為null,則新節點將插在當前節點內部的最后位置,即變成最后一個子節點

var p = document.createElement('p'); document.body.insertBefore(p, null);

上面代碼中,p將成為document.body的最后一個子節點。這也說明insertBefore的第二個參數不能省略。

注意,如果所要插入的節點是當前 DOM 現有的節點,則該節點將從原有的位置移除,插入新的位置。

由于不存在insertAfter方法,如果新節點要插在父節點的某個子節點后面,可以用insertBefore方法結合nextSibling屬性模擬

parent.insertBefore(s1, s2.nextSibling);

上面代碼中,parent是父節點,s1是一個全新的節點,s2是可以將s1節點,插在s2節點的后面。如果s2是當前節點的最后一個子節點,則s2.nextSibling返回null,這時s1節點會插在當前節點的最后,變成當前節點的最后一個子節點,等于緊跟在s2的后面。如果要插入的節點是DocumentFragment類型,那么插入的將是DocumentFragment的所有子節點,而不是DocumentFragment節點本身。返回值將是一個空的DocumentFragment節點

Node.prototype.removeChild()

removeChild方法接受一個子節點作為參數,用于從當前節點移除該子節點。返回值是移除的子節點

var divA = document.getElementById('A'); divA.parentNode.removeChild(divA);

上面代碼移除了divA節點。注意,這個方法是在divA的父節點上調用的,不是在divA上調用的。下面是如何移除當前節點的所有子節點

var element = document.getElementById('top'); while (element.firstChild) { element.removeChild(element.firstChild); }

被移除的節點依然存在于內存之中,但不再是 DOM 的一部分。所以,一個節點移除以后,依然可以使用它,比如插入到另一個節點下面。如果參數節點不是當前節點的子節點,removeChild方法將報錯

Node.prototype.replaceChild()

replaceChild方法用于將一個新的節點,替換當前節點的某一個子節點

var replacedNode = parentNode.replaceChild(newChild, oldChild);

上面代碼中,replaceChild方法接受兩個參數,第一個參數newChild是用來替換的新節點,第二個參數oldChild是將要替換走的子節點。返回值是替換走的那個節點oldChild

var divA = document.getElementById('divA'); var newSpan = document.createElement('span'); newSpan.textContent = 'Hello World!'; divA.parentNode.replaceChild(newSpan, divA);

Node.prototype.contains()

contains方法返回一個布爾值,表示參數節點是否滿足以下三個條件之一:

1.參數節點為當前節點

2.參數節點為當前節點的子節點

3.參數節點為當前節點的后代節點

document.body.contains(node)

上面代碼檢查參數節點node,是否包含在當前文檔之中

注意,當前節點傳入contains方法,返回true

nodeA.contains(nodeA) // true

Node.prototype.compareDocumentPosition()

compareDocumentPosition方法的用法,與contains方法完全一致,返回一個六個比特位的二進制值,表示參數節點與當前節點的關系

二進制值十進制值含義0000000兩個節點相同0000011兩個節點不在同一個文檔(即有一個節點不在當前文檔)0000102參數節點在當前節點的前面0001004參數節點在當前節點的后面0010008參數節點包含當前節點01000016當前節點包含參數節點10000032瀏覽器內部使用<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>

上面代碼中,節點div包含節點input(二進制010000),而且節點input在節點div的后面(二進制000100),所以第一個compareDocumentPosition方法返回20(二進制010100,即010000 + 000100),第二個compareDocumentPosition方法返回10(二進制001010)。由于compareDocumentPosition返回值的含義,定義在每一個比特位上,所以如果要檢查某一種特定的含義,就需要使用比特位運算符

var head = document.head; var body = document.body; if (head.compareDocumentPosition(body) & 4) {console.log('文檔結構正確'); } else {console.log('<body> 不能在 <head> 前面'); }

上面代碼中,compareDocumentPosition的返回值與4(又稱掩碼)進行與運算(&),得到一個布爾值,表示

是否在前面

Node.prototype.isEqualNode(),Node.prototype.isSameNode()

isEqualNode方法返回一個布爾值,用于檢查兩個節點是否相等。所謂相等的節點,指的是兩個節點的類型相同、屬性相同、子節點相同

var p1 = document.createElement('p'); var p2 = document.createElement('p'); p1.isEqualNode(p2) // true

isSameNode方法返回一個布爾值,表示兩個節點是否為同一個節點

var p1 = document.createElement('p'); var p2 = document.createElement('p'); p1.isSameNode(p2) // false p1.isSameNode(p1) // true

Node.prototype.normalize()

normalize方法用于清理當前節點內部的所有文本節點(text)。它會去除空的文本節點,并且將毗鄰的文本節點合并成一個,也就是說不存在空的文本節點,以及毗鄰的文本節點

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節點有兩個毗鄰的文本子節點。使用normalize方法之后,兩個文本子節點被合并成一個;該方法是Text.splitText的逆方法

Node.prototype.getRootNode()

getRootNode()方法返回當前節點所在文檔的根節點document,與ownerDocument屬性的作用相同

document.body.firstChild.getRootNode() === document // true document.body.firstChild.getRootNode() === document.body.firstChild.ownerDocument // true

該方法可用于document節點自身,這一點與document.ownerDocument不同

document.getRootNode() // document document.ownerDocument // null

NodeList 接口,HTMLCollection 接口

節點都是單個對象,有時需要一種數據結構,能夠容納多個節點。DOM 提供兩種節點集合,用于容納多個節點:NodeList和HTMLCollection。這兩種集合都屬于接口規范;許多 DOM 屬性和方法,返回的結果是NodeList實例或HTMLCollection實例。主要區別是,NodeList可以包含各種類型的節點,HTMLCollection只能包含 HTML 元素節點

NodeList 接口

概述

NodeList實例是一個類似數組的對象,它的成員是節點對象。通過以下方法可以得到NodeList實例:

1.Node.childNodes

2.document.querySelectorAll()等節點搜索方法

document.body.childNodes instanceof NodeList // true

NodeList實例很像數組,可以使用length屬性和forEach方法。但是,它不是數組,不能使用pop或push之類數組特有的方法

var children = document.body.childNodes; Array.isArray(children) // false children.length // 34 children.forEach(console.log)

上面代碼中,NodeList 實例children不是數組,但是具有length屬性和forEach方法。如果NodeList實例要使用數組方法,可以將其轉為真正的數組

var children = document.body.childNodes; var nodeArr = Array.prototype.slice.call(children);

除了使用forEach方法遍歷 NodeList 實例,還可以使用for循環

var children = document.body.childNodes; for (var i = 0; i < children.length; i++) { var item = children[i]; }

注意,NodeList 實例可能是動態集合,也可能是靜態集合。所謂動態集合就是一個活的集合,DOM 刪除或新增一個相關節點,都會立刻反映在 NodeList 實例。目前,只有Node.childNodes返回的是一個動態集合,其他的 NodeList 都是靜態集合

var children = document.body.childNodes; children.length // 18 document.body.appendChild(document.createElement('p')); children.length // 19

上面代碼中,文檔增加一個子節點,NodeList 實例children的length屬性就增加了1

NodeList.prototype.length

length屬性返回 NodeList 實例包含的節點數量

document.querySelectorAll('xxx').length // 0

上面代碼中,document.querySelectorAll返回一個 NodeList 集合。對于那些不存在的 HTML 標簽,length屬性返回0

NodeList.prototype.forEach()

forEach方法用于遍歷 NodeList 的所有成員。它接受一個回調函數作為參數,每一輪遍歷就執行一次這個回調函數,用法與數組實例的forEach方法完全一致

var children = document.body.childNodes; children.forEach(function f(item, i, list) {// ... }, this);

上面代碼中,回調函數f的三個參數依次是當前成員、位置和當前 NodeList 實例。forEach方法的第二個參數,用于綁定回調函數內部的this,該參數可省略

NodeList.prototype.item()

item方法接受一個整數值作為參數,表示成員的位置,返回該位置上的成員

document.body.childNodes.item(0)

上面代碼中,item(0)返回第一個成員;如果參數值大于實際長度,或者索引不合法(比如負數),item方法返回null;如果省略參數,item方法會報錯;所有類似數組的對象,都可以使用方括號運算符取出成員;一般情況下,都是使用方括號運算符,而不使用item方法

document.body.childNodes[0]

NodeList.prototype.keys(),NodeList.prototype.values(),NodeList.prototype.entries()

這三個方法都返回一個 ES6 的遍歷器對象,可以通過for...of循環遍歷獲取每一個成員的信息。區別在于,keys()返回鍵名的遍歷器,values()返回鍵值的遍歷器,entries()返回的遍歷器同時包含鍵名和鍵值的信息

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是一個節點對象的集合,只能包含元素節點(element),不能包含其他類型的節點。它的返回值是一個類似數組的對象,但是與NodeList接口不同,HTMLCollection沒有forEach方法,只能使用for循環遍歷。返回HTMLCollection實例的,主要是一些Document對象的集合屬性,比如document.links、document.forms、document.images等

document.links instanceof HTMLCollection // true

HTMLCollection實例都是動態集合,節點的變化會實時反映在集合中;如果元素節點有id或name屬性,那么HTMLCollection實例上面,可以使用id屬性或name屬性引用該節點元素。如果沒有對應的節點,則返回null

<img id="pic" src="http://example.com/foo.jpg"> var pic = document.getElementById('pic'); document.images.pic === pic // true

上面代碼中,document.images是一個HTMLCollection實例,可以通過元素的id屬性值,從HTMLCollection實例上取到這個元素

HTMLCollection.prototype.length

length屬性返回HTMLCollection實例包含的成員數量

document.links.length // 18

HTMLCollection.prototype.item()

item方法接受一個整數值作為參數,表示成員的位置,返回該位置上的成員

var c = document.images; var img0 = c.item(0);

上面代碼中,item(0)表示返回0號位置的成員。由于方括號運算符也具有同樣作用,而且使用更方便,所以一般情況下,總是使用方括號運算符;如果參數值超出成員數量或者不合法(比如小于0),那么item方法返回null

HTMLCollection.prototype.namedItem()

namedItem方法的參數是一個字符串,表示id屬性或name屬性的值,返回對應的元素節點。如果沒有對應的節點,則返回null

<img id="pic" src="http://example.com/foo.jpg"> var pic = document.getElementById('pic'); document.images.namedItem('pic') === pic // true

ParentNode 接口,ChildNode 接口

節點對象除了繼承 Node 接口以外,還會繼承其他接口。ParentNode接口表示當前節點是一個父節點,提供一些處理子節點的方法;ChildNode接口表示當前節點是一個子節點,提供一些相關方法

ParentNode 接口

如果當前節點是父節點,就會繼承ParentNode接口。由于只有元素節點(element)、文檔節點(document)和文檔片段節點(documentFragment)擁有子節點,因此只有這三類節點會繼承ParentNode接口

ParentNode.children

children屬性返回一個HTMLCollection實例,成員是當前節點的所有元素子節點,該屬性只讀;下面是遍歷某個節點的所有元素子節點的示例

for (var i = 0; i < el.children.length; i++) {// ... }

注意,children屬性只包括元素子節點,不包括其他類型的子節點(比如文本子節點);如果沒有元素類型的子節點,返回值HTMLCollection實例的length屬性為0。另外,HTMLCollection是動態集合,會實時反映 DOM 的任何變化

ParentNode.firstElementChild

firstElementChild屬性返回當前節點的第一個元素子節點;如果沒有任何元素子節點,則返回null

document.firstElementChild.nodeName // "HTML"

上面代碼中,document節點的第一個元素子節點是

ParentNode.lastElementChild

lastElementChild屬性返回當前節點的最后一個元素子節點,如果不存在任何元素子節點,則返回null

document.lastElementChild.nodeName // "HTML"

上面代碼中,document節點的最后一個元素子節點是(因為document只包含這一個元素子節點)

ParentNode.childElementCount

childElementCount屬性返回一個整數,表示當前節點的所有元素子節點的數目。如果不包含任何元素子節點,則返回0

document.body.childElementCount // 13

ParentNode.append(),ParentNode.prepend()

append方法為當前節點追加一個或多個子節點,位置是最后一個元素子節點的后面;該方法不僅可以添加元素子節點,還可以添加文本子節點

var parent = document.body; // 添加元素子節點 var p = document.createElement('p'); parent.append(p); // 添加文本子節點 parent.append('Hello'); // 添加多個元素子節點 var p1 = document.createElement('p'); var p2 = document.createElement('p'); parent.append(p1, p2); // 添加元素子節點和文本子節點 var p = document.createElement('p'); parent.append('Hello', p);

注意,該方法沒有返回值;prepend方法為當前節點追加一個或多個子節點,位置是第一個元素子節點的前面;它的用法與append方法完全一致,也是沒有返回值

ChildNode 接口

如果一個節點有父節點,那么該節點就繼承了ChildNode接口

ChildNode.remove()

remove方法用于從父節點移除當前節點

el.remove()

上面代碼在 DOM 里面移除了el節點

ChildNode.before(),ChildNode.after()

before方法用于在當前節點的前面,插入一個或多個同級節點,兩者擁有相同的父節點;注意,該方法不僅可以插入元素節點,還可以插入文本節點

var p = document.createElement('p'); var p1 = document.createElement('p'); el.before(p); // 插入元素節點 el.before('Hello'); // 插入文本節點 el.before(p, p1); // 插入多個元素節點 el.before(p, 'Hello'); // 插入元素節點和文本節點

after方法用于在當前節點的后面,插入一個或多個同級節點,兩者擁有相同的父節點。用法與before方法完全相同

ChildNode.replaceWith()

replaceWith方法使用參數節點,替換當前節點。參數可以是元素節點,也可以是文本節點

var span = document.createElement('span'); el.replaceWith(span);

上面代碼中,el節點將被span節點替換

Document 節點

概述

document節點對象代表整個文檔,每張網頁都有自己的document對象,window.document屬性就指向這個對象;只要瀏覽器開始載入 HTML 文檔,該對象就存在了,可以直接使用。document對象有不同的辦法可以獲取:

1.正常的網頁,直接使用document或window.document

2.iframe框架里面的網頁,使用iframe節點的contentDocument屬性

3.Ajax 操作返回的文檔,使用XMLHttpRequest對象的responseXML屬性

4.內部節點的ownerDocument屬性

document對象繼承了EventTarget接口、Node接口、ParentNode接口;這意味著這些接口的方法都可以在document對象上調用;除此之外,document對象還有很多自己的屬性和方法

屬性

快捷方式屬性

以下屬性是指向文檔內部的某個節點的快捷方式

document.defaultView

document.defaultView屬性返回document對象所屬的window對象。如果當前文檔不屬于window對象,該屬性返回null

document.defaultView === window // true
document.doctype

對于 HTML 文檔來說,document對象一般有兩個子節點;第一個子節點是document.doctype,指向節點,即文檔類型(Document Type Declaration,簡寫DTD)節點;HTML 的文檔類型節點一般寫成,如果網頁沒有聲明 DTD,該屬性返回null

var doctype = document.doctype; doctype // "<!DOCTYPE html>" doctype.name // "html"

document.firstChild通常就返回這個節點

document.documentElement

document.documentElement屬性返回當前文檔的根元素節點(root);它通常是document節點的第二個子節點,緊跟在document.doctype節點后面;HTML網頁的該屬性,一般是節點

document.body,document.head

document.body屬性指向

節點,document.head屬性指向節點;這兩個屬性總是存在的,如果網頁源碼里面省略了或,瀏覽器會自動創建;另外,這兩個屬性是可寫的,如果改寫它們的值,相當于移除所有子節點
document.scrollingElement

document.scrollingElement屬性返回文檔的滾動元素。也就是說,當文檔整體滾動時,到底是哪個元素在滾動;標準模式下,這個屬性返回的文檔的根元素document.documentElement(即)。兼容(quirk)模式下,返回的是

元素,如果該元素不存在,返回nulldocument.scrollingElement.scrollTop = 0; // 頁面滾動到瀏覽器頂部
document.activeElement

document.activeElement屬性返回獲得當前焦點(focus)的 DOM 元素。通常,這個屬性返回的是、、等表單元素;如果當前沒有焦點元素,返回元素或null

document.fullscreenElement

document.fullscreenElement屬性返回當前以全屏狀態展示的 DOM 元素。如果不是全屏狀態,該屬性返回null

if (document.fullscreenElement.nodeName == 'VIDEO') { console.log('全屏播放視頻'); }

上面代碼中,通過document.fullscreenElement可以知道

元素有沒有處在全屏狀態,從而判斷用戶行為

節點集合屬性

以下屬性返回一個HTMLCollection實例,表示文檔內部特定元素的集合;這些集合都是動態的,原節點有任何變化,立刻會反映在集合中

document.links

document.links屬性返回當前文檔所有設定了href屬性的

// 打印文檔所有的鏈接 var links = document.links; for(var i = 0; i < links.length; i++) {console.log(links[i]); }
document.forms

document.forms屬性返回所有

表單節點var selectForm = document.forms[0];

上面代碼獲取文檔第一個表單;除了使用位置序號,id屬性和name屬性也可以用來引用表單

<form name="foo" id="bar"></form> document.forms[0] === document.forms.foo // true document.forms.bar === document.forms.foo // true
document.images

document.images屬性返回頁面所有圖片節點

var imglist = document.images; for(var i = 0; i < imglist.length; i++) {if (imglist[i].src === 'banner.gif') {// ...} }

上面代碼在所有img標簽中,尋找某張圖片

document.embeds,document.plugins

document.embeds屬性和document.plugins屬性,都返回所有節點

document.scripts

document.scripts屬性返回所有

var scripts = document.scripts; if (scripts.length !== 0 ) { console.log('當前網頁有腳本'); }
document.styleSheets

document.styleSheets屬性返回文檔內嵌或引入的樣式表集合

小結

除了document.styleSheets,以上的集合屬性返回的都是HTMLCollection實例

document.links instanceof HTMLCollection // true document.images instanceof HTMLCollection // true document.forms instanceof HTMLCollection // true document.embeds instanceof HTMLCollection // true document.scripts instanceof HTMLCollection // true

HTMLCollection實例是類似數組的對象,所以這些屬性都有length屬性,都可以使用方括號運算符引用成員。如果成員有id或name屬性,還可以用這兩個屬性的值,在HTMLCollection實例上引用到這個成員

<form name="myForm"> document.myForm === document.forms.myForm // true

文檔靜態信息屬性

以下屬性返回文檔信息

document.documentURI,document.URL

document.documentURI屬性和document.URL屬性都返回一個字符串,表示當前文檔的網址;不同之處是它們繼承自不同的接口,documentURI繼承自Document接口,可用于所有文檔;URL繼承自HTMLDocument接口,只能用于 HTML 文檔

document.URL // http://www.example.com/about document.documentURI === document.URL // true

如果文檔的錨點(#anchor)變化,這兩個屬性都會跟著變化

document.domain

document.domain屬性返回當前文檔的域名,不包含協議和接口;比如,網頁的網址是,那么domain屬性就等于www.example.com。如果無法獲取域名,該屬性返回null;document.domain基本上是一個只讀屬性,只有一種情況除外;次級域名的網頁,可以把document.domain設為對應的上級域名。比如,當前域名是a.sub.example.com,則document.domain屬性可以設置為sub.example.com,也可以設為example.com。修改后,document.domain相同的兩個網頁,可以讀取對方的資源,比如設置的 Cookie。

另外,設置document.domain會導致端口被改成null。因此,如果通過設置document.domain來進行通信,雙方網頁都必須設置這個值,才能保證端口相同

document.location

Location對象是瀏覽器提供的原生對象,提供 URL 相關的信息和操作方法。通過window.location和document.location屬性,可以拿到這個對象

document.lastModified

document.lastModified屬性返回一個字符串,表示當前文檔最后修改的時間。不同瀏覽器的返回值,日期格式是不一樣的

document.lastModified // "04/10/2019 12:17:01"

注意:document.lastModified屬性的值是字符串,所以不能直接用來比較;Date.parse方法將其轉為Date實例,才能比較兩個網頁

var lastVisitedDate = Date.parse('01/01/2018'); if (Date.parse(document.lastModified) > lastVisitedDate) { console.log('網頁已經變更'); }
document.characterSet

document.characterSet屬性返回當前文檔的編碼,比如UTF-8、ISO-8859-1等

document.referrer

document.referrer屬性返回一個字符串,表示當前文檔的訪問者來自哪里

document.referrer // "https://example.com/path"

如果無法獲取來源,或者用戶直接鍵入網址而不是從其他網頁點擊進入,document.referrer返回一個空字符串;document.referrer的值,總是與 HTTP 頭信息的Referer字段保持一致。但是,document.referrer的拼寫有兩個r,而頭信息的Referer字段只有一個r

document.dir

document.dir返回一個字符串,表示文字方向。它只有兩個可能的值:rtl表示文字從右到左,阿拉伯文是這種方式;ltr表示文字從左到右,包括英語和漢語在內的大多數文字采用這種方式

document.compatMode

compatMode屬性返回瀏覽器處理文檔的模式,可能的值為BackCompat(向后兼容模式)和CSS1Compat(嚴格模式)。一般來說,如果網頁代碼的第一行設置了明確的DOCTYPE(比如),document.compatMode的值都為CSS1Compat

文檔狀態屬性

document.hidden

document.hidden屬性返回一個布爾值,表示當前頁面是否可見;如果窗口最小化、瀏覽器切換了 Tab,都會導致頁面不可見,使得document.hidden返回true。這個屬性是 Page Visibility API 引入的,一般都是配合這個 API 使用

document.visibilityState

document.visibilityState返回文檔的可見狀態;它的值有四種可能:

1.visible:頁面可見。注意,頁面可能是部分可見,即不是焦點窗口,前面被其他窗口部分擋住了

2.hidden:頁面不可見,有可能窗口最小化,或者瀏覽器切換到了另一個 Tab

3.prerender:頁面處于正在渲染狀態,對于用戶來說,該頁面不可見

4.unloaded:頁面從內存里面卸載了

這個屬性可以用在頁面加載時,防止加載某些資源;或者頁面不可見時停掉一些頁面功能

document.readyState

document.readyState屬性返回當前文檔的狀態,共有三種可能的值:

1.loading:加載 HTML 代碼階段(尚未完成解析)

2.interactive:加載外部資源階段

3.complete:加載完成

這個屬性變化的過程如下:

1.瀏覽器開始解析 HTML 文檔,document.readyState屬性等于loading

2.瀏覽器遇到 HTML 文檔中的

3.HTML 文檔解析完成,document.readyState屬性變成interactive

4.瀏覽器等待圖片、樣式表、字體文件等外部資源加載完成;一旦全部加載完成,document.readyState屬性變成complete

下面的代碼用來檢查網頁是否加載成功

if (document.readyState === 'complete') { // 基本檢查// ... } var interval = setInterval(function() { // 輪詢檢查if (document.readyState === 'complete') {clearInterval(interval);// ...} }, 100);

另外,每次狀態變化都會觸發一個readystatechange事件

document.cookie

document.cookie屬性用來操作瀏覽器 Cookie

document.designMode

document.designMode屬性控制當前文檔是否可編輯。該屬性只有兩個值on和off,默認值為off。一旦設為on,用戶就可以編輯整個文檔的內容;

document.implementation

document.implementation屬性返回一個DOMImplementation對象。該對象有三個方法,主要用于創建獨立于當前文檔的新的 Document 對象;

1.DOMImplementation.createDocument():創建一個 XML 文檔

2.DOMImplementation.createHTMLDocument():創建一個 HTML 文檔

3.DOMImplementation.createDocumentType():創建一個 DocumentType 對象

下面是創建 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 );

上面代碼中,第一步生成一個新的 HTML 文檔doc,然后用它的根元素document.documentElement替換掉document.documentElement;這會使得當前文檔的內容全部消失,變成hello world

方法

document.open(),document.close()

document.open方法清除當前文檔所有內容,使得文檔處于可寫狀態,供document.write方法寫入內容;document.close方法用來關閉document.open()打開的文檔

document.open(); document.write('hello world'); document.close();

document.write(),document.writeln()

document.write方法用于向當前文檔寫入內容;在網頁的首次渲染階段,只要頁面沒有關閉寫入(即沒有執行document.close()),document.write寫入的內容就會追加在已有內容的后面

// 頁面顯示“helloworld” document.open(); document.write('hello'); document.write('world'); document.close();

注意,document.write會當作 HTML 代碼解析,不會轉義

document.write('<p>hello world</p>');

上面代碼中,document.write會將

當作 HTML 標簽解釋;如果頁面已經解析完成(DOMContentLoaded事件發生之后),再調用write方法,它會先調用open方法,擦除當前文檔所有內容,然后再寫入

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(); });

如果在頁面渲染過程中調用write方法,并不會自動調用open方法。(可以理解成,open方法已調用,但close方法還未調用。)

<html><body> hello <script type="text/javascript">document.write("world") </script> </body></html>

在瀏覽器打開上面網頁,將會顯示hello world;document.write是 JavaScript 語言標準化之前就存在的方法,現在完全有更符合標準的方法向文檔寫入內容(比如對innerHTML屬性賦值);所以,除了某些特殊情況,應該盡量避免使用document.write這個方法;document.writeln方法與write方法完全一致,除了會在輸出內容的尾部添加換行符

document.write(1); document.write(2); // 12 document.writeln(1); document.writeln(2); // 1 // 2

注意,writeln方法添加的是 ASCII 碼的換行符,渲染成 HTML 網頁時不起作用,即在網頁上顯示不出換行。網頁上的換行,必須顯式寫入

document.querySelector(),document.querySelectorAll()

document.querySelector方法接受一個 CSS 選擇器作為參數,返回匹配該選擇器的元素節點;如果有多個節點滿足匹配條件,則返回第一個匹配的節點;如果沒有發現匹配的節點,則返回null

var el1 = document.querySelector('.myclass'); var el2 = document.querySelector('#myParent > [ng-click]');

document.querySelectorAll方法與querySelector用法類似,區別是返回一個NodeList對象,包含所有匹配給定選擇器的節點

elementList = document.querySelectorAll('.myclass');

這兩個方法的參數,可以是逗號分隔的多個 CSS 選擇器,返回匹配其中一個選擇器的元素節點,這與 CSS 選擇器的規則是一致的

var matches = document.querySelectorAll('div.note, div.alert');

上面代碼返回class屬性是note或alert的div元素;這兩個方法都支持復雜的 CSS 選擇器

document.querySelectorAll('[data-foo-bar="someval"]'); // 選中 data-foo-bar 屬性等于 someval 的元素 document.querySelectorAll('#myForm :invalid'); // 選中 myForm 表單中所有不通過驗證的元素 document.querySelectorAll('DIV:not(.ignore)'); // 選中div元素,那些 class 含 ignore 的除外 document.querySelectorAll('DIV, A, SCRIPT'); // 同時選中 div,a,script 三類元素

但是,它們不支持 CSS 偽元素的選擇器(比如:first-line和:first-letter)和偽類的選擇器(比如:link和:visited),即無法選中偽元素和偽類;如果querySelectorAll方法的參數是字符串*,則會返回文檔中的所有元素節點;另外,querySelectorAll的返回結果不是動態集合,不會實時反映元素節點的變化;最后,這兩個方法除了定義在document對象上,還定義在元素節點上,即在元素節點上也可以調用

document.getElementsByTagName()

document.getElementsByTagName方法搜索 HTML 標簽名,返回符合條件的元素。它的返回值是一個類似數組對象(HTMLCollection實例),可以實時反映 HTML 文檔的變化。如果沒有任何匹配的元素,就返回一個空集

var paras = document.getElementsByTagName('p'); paras instanceof HTMLCollection // true

上面代碼返回當前文檔的所有p元素節點;HTML 標簽名是大小寫不敏感的,因此getElementsByTagName方法也是大小寫不敏感的。另外,返回結果中,各個成員的順序就是它們在文檔中出現的順序;如果傳入*,就可以返回文檔中所有 HTML 元素

var allElements = document.getElementsByTagName('*');

注意,元素節點本身也定義了getElementsByTagName方法,返回該元素的后代元素中符合條件的元素。也就是說,這個方法不僅可以在document對象上調用,也可以在任何元素節點上調用

var firstPara = document.getElementsByTagName('p')[0]; var spans = firstPara.getElementsByTagName('span');

上面代碼選中第一個p元素內部的所有span元素

document.getElementsByClassName()

document.getElementsByClassName方法返回一個類似數組的對象(HTMLCollection實例),包括了所有class名字符合指定條件的元素,元素的變化實時反映在返回結果中

var elements = document.getElementsByClassName(names);

由于class是保留字,所以 JavaScript 一律使用className表示 CSS 的class;參數可以是多個class,它們之間使用空格分隔

var elements = document.getElementsByClassName('foo bar');

上面代碼返回同時具有foo和bar兩個class的元素,foo和bar的順序不重要。;注意,正常模式下,CSS 的class是大小寫敏感的;(quirks mode下,大小寫不敏感。)

與getElementsByTagName方法一樣,getElementsByClassName方法不僅可以在document對象上調用,也可以在任何元素節點上調用

// 非document對象上調用 var elements = rootElement.getElementsByClassName(names);

document.getElementsByName()

document.getElementsByName方法用于選擇擁有name屬性的 HTML 元素(比如

、、、、和

總結

以上是生活随笔為你收集整理的JavaScript 教程(二)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

中文字幕 人妻熟女 | 亚洲精品无码国产 | 午夜精品一区二区三区在线观看 | 色综合久久中文娱乐网 | 久在线观看福利视频 | 国产欧美精品一区二区三区 | 澳门永久av免费网站 | 精品久久久无码人妻字幂 | 亚拍精品一区二区三区探花 | 午夜熟女插插xx免费视频 | 日韩精品乱码av一区二区 | 亚洲国产av美女网站 | 午夜丰满少妇性开放视频 | 国产亚洲人成a在线v网站 | 亚洲精品成a人在线观看 | 97久久精品无码一区二区 | 色婷婷综合中文久久一本 | 最近中文2019字幕第二页 | 伊人久久大香线蕉亚洲 | 极品嫩模高潮叫床 | 中文字幕人妻无码一区二区三区 | 18无码粉嫩小泬无套在线观看 | 精品国产av色一区二区深夜久久 | 正在播放老肥熟妇露脸 | 国产成人精品视频ⅴa片软件竹菊 | 日韩精品成人一区二区三区 | 亚洲国产精品久久久天堂 | 亚洲国产av精品一区二区蜜芽 | 极品尤物被啪到呻吟喷水 | 国产精品久久久 | 日产精品高潮呻吟av久久 | 窝窝午夜理论片影院 | 亚洲国产欧美国产综合一区 | a片在线免费观看 | 久久久久亚洲精品中文字幕 | 扒开双腿疯狂进出爽爽爽视频 | 丝袜美腿亚洲一区二区 | 丰满妇女强制高潮18xxxx | 国产电影无码午夜在线播放 | 中文毛片无遮挡高清免费 | 色婷婷综合中文久久一本 | 日本一卡2卡3卡4卡无卡免费网站 国产一区二区三区影院 | 国产无遮挡吃胸膜奶免费看 | 激情内射亚州一区二区三区爱妻 | 亚洲精品欧美二区三区中文字幕 | 天下第一社区视频www日本 | 狠狠色丁香久久婷婷综合五月 | 精品国精品国产自在久国产87 | 中文无码精品a∨在线观看不卡 | 风流少妇按摩来高潮 | 亚洲精品国产精品乱码不卡 | 久久国产精品萌白酱免费 | 丰满人妻一区二区三区免费视频 | 国产精品永久免费视频 | 骚片av蜜桃精品一区 | 无码av最新清无码专区吞精 | 亚洲综合无码一区二区三区 | 中文无码成人免费视频在线观看 | 日本精品高清一区二区 | 久久五月精品中文字幕 | 麻豆精品国产精华精华液好用吗 | 一二三四社区在线中文视频 | 国产麻豆精品一区二区三区v视界 | 国产乱人伦av在线无码 | 国内揄拍国内精品少妇国语 | 最近免费中文字幕中文高清百度 | 亚洲另类伦春色综合小说 | 久久人人97超碰a片精品 | 九月婷婷人人澡人人添人人爽 | 国产免费久久精品国产传媒 | 2020久久香蕉国产线看观看 | 亚洲天堂2017无码中文 | 欧美精品在线观看 | 狠狠cao日日穞夜夜穞av | 蜜桃视频插满18在线观看 | 精品国产麻豆免费人成网站 | 人妻人人添人妻人人爱 | 动漫av一区二区在线观看 | 窝窝午夜理论片影院 | 国产精品久久久久久久影院 | 亚洲色欲色欲天天天www | 蜜桃av蜜臀av色欲av麻 999久久久国产精品消防器材 | 18无码粉嫩小泬无套在线观看 | 中文字幕无码乱人伦 | 亚洲精品一区二区三区四区五区 | 99在线 | 亚洲 | 精品国产一区二区三区四区 | 国精产品一区二区三区 | 久久精品国产99精品亚洲 | 国产在线精品一区二区高清不卡 | 少妇的肉体aa片免费 | 成年美女黄网站色大免费视频 | 99久久久国产精品无码免费 | 久久久久av无码免费网 | 亚洲性无码av中文字幕 | 国产女主播喷水视频在线观看 | 亚洲国产精品一区二区美利坚 | 国产精品二区一区二区aⅴ污介绍 | 十八禁视频网站在线观看 | 激情五月综合色婷婷一区二区 | 国产人成高清在线视频99最全资源 | 欧美精品一区二区精品久久 | 在线看片无码永久免费视频 | 亚洲成av人在线观看网址 | 人妻少妇精品久久 | 色 综合 欧美 亚洲 国产 | 最新国产麻豆aⅴ精品无码 | 人妻夜夜爽天天爽三区 | 中文字幕无码视频专区 | 樱花草在线社区www | 中文字幕乱码人妻无码久久 | 成人动漫在线观看 | 国产精品久久久久无码av色戒 | 精品人人妻人人澡人人爽人人 | 精品aⅴ一区二区三区 | 久久人人爽人人人人片 | 国产suv精品一区二区五 | 六月丁香婷婷色狠狠久久 | 国产精品无码一区二区桃花视频 | 97精品国产97久久久久久免费 | 色综合久久久无码中文字幕 | 无码国模国产在线观看 | 国产一精品一av一免费 | 性色欲情网站iwww九文堂 | 麻豆av传媒蜜桃天美传媒 | 午夜理论片yy44880影院 | 欧美成人高清在线播放 | 97精品人妻一区二区三区香蕉 | 国产肉丝袜在线观看 | 日韩av无码一区二区三区 | 国产精品无码永久免费888 | 日韩精品无码一本二本三本色 | 欧美精品一区二区精品久久 | 又紧又大又爽精品一区二区 | 久久久久99精品成人片 | 精品国产精品久久一区免费式 | 国产精品亚洲专区无码不卡 | 成人亚洲精品久久久久软件 | 免费观看的无遮挡av | 成人片黄网站色大片免费观看 | 亚洲午夜无码久久 | 国内揄拍国内精品少妇国语 | 国产成人无码区免费内射一片色欲 | 亚洲熟妇色xxxxx欧美老妇 | 亚洲一区二区三区四区 | 扒开双腿疯狂进出爽爽爽视频 | 美女扒开屁股让男人桶 | 精品无码国产自产拍在线观看蜜 | 99久久亚洲精品无码毛片 | 青青久在线视频免费观看 | 精品国产一区二区三区四区在线看 | 日韩精品a片一区二区三区妖精 | 欧美大屁股xxxxhd黑色 | 日韩成人一区二区三区在线观看 | 色噜噜亚洲男人的天堂 | 人妻互换免费中文字幕 | 最新国产乱人伦偷精品免费网站 | 中文字幕色婷婷在线视频 | 76少妇精品导航 | 精品国产成人一区二区三区 | 亚洲精品一区国产 | 亚洲乱码国产乱码精品精 | 日本乱人伦片中文三区 | 人人妻人人澡人人爽欧美一区 | 成人精品天堂一区二区三区 | 乱人伦人妻中文字幕无码久久网 | 99久久人妻精品免费二区 | 熟妇女人妻丰满少妇中文字幕 | 日韩人妻无码一区二区三区久久99 | 三上悠亚人妻中文字幕在线 | 国色天香社区在线视频 | 国产亚洲人成a在线v网站 | 国产午夜精品一区二区三区嫩草 | 国产农村妇女高潮大叫 | 欧美性猛交内射兽交老熟妇 | 久久精品视频在线看15 | 无码一区二区三区在线 | 亚洲精品欧美二区三区中文字幕 | 精品人妻中文字幕有码在线 | 国产av无码专区亚洲a∨毛片 | 国产麻豆精品一区二区三区v视界 | 永久黄网站色视频免费直播 | 男女作爱免费网站 | 久精品国产欧美亚洲色aⅴ大片 | 人妻中文无码久热丝袜 | 夜夜高潮次次欢爽av女 | 国产精品久久久 | 日日天日日夜日日摸 | 国产va免费精品观看 | 精品亚洲成av人在线观看 | 久久国产自偷自偷免费一区调 | 丰满少妇弄高潮了www | 久久99精品久久久久久 | 亚洲日韩av片在线观看 | 国产在线一区二区三区四区五区 | 欧美日韩视频无码一区二区三 | 日韩精品久久久肉伦网站 | 亚洲色大成网站www | 性欧美videos高清精品 | 国产欧美精品一区二区三区 | 亚洲国产综合无码一区 | 久久97精品久久久久久久不卡 | 国产av一区二区三区最新精品 | 波多野结衣一区二区三区av免费 | 成 人 免费观看网站 | 97久久国产亚洲精品超碰热 | 精品一区二区三区波多野结衣 | 精品无码一区二区三区的天堂 | 东京热无码av男人的天堂 | 亚洲国产综合无码一区 | 国产三级精品三级男人的天堂 | 国产高潮视频在线观看 | 台湾无码一区二区 | 色综合天天综合狠狠爱 | 中文字幕无线码 | 亚洲中文无码av永久不收费 | 欧美老熟妇乱xxxxx | 成人亚洲精品久久久久软件 | 日本精品人妻无码77777 天堂一区人妻无码 | 成人女人看片免费视频放人 | 日本丰满护士爆乳xxxx | 亚洲 激情 小说 另类 欧美 | 大肉大捧一进一出视频出来呀 | 免费无码午夜福利片69 | 色偷偷人人澡人人爽人人模 | 欧美国产日韩亚洲中文 | 中文字幕人妻无码一区二区三区 | 久久久精品国产sm最大网站 | 欧美freesex黑人又粗又大 | 午夜福利一区二区三区在线观看 | 国产网红无码精品视频 | 国产一区二区三区日韩精品 | 国产后入清纯学生妹 | 久久国产36精品色熟妇 | 又色又爽又黄的美女裸体网站 | 国产精品手机免费 | 精品久久久中文字幕人妻 | 成人毛片一区二区 | 欧美日韩人成综合在线播放 | 综合网日日天干夜夜久久 | 国产肉丝袜在线观看 | 日日摸夜夜摸狠狠摸婷婷 | 亚洲国产精品无码久久久久高潮 | 成人综合网亚洲伊人 | 樱花草在线社区www | 国产艳妇av在线观看果冻传媒 | 国产性生大片免费观看性 | 久久国语露脸国产精品电影 | 麻豆蜜桃av蜜臀av色欲av | 天堂а√在线中文在线 | 又色又爽又黄的美女裸体网站 | 国产人妻精品午夜福利免费 | 欧美精品在线观看 | 少妇邻居内射在线 | 牲欲强的熟妇农村老妇女 | 国产在线精品一区二区高清不卡 | 午夜精品久久久内射近拍高清 | 中文字幕+乱码+中文字幕一区 | 国产亚洲人成在线播放 | 亚洲啪av永久无码精品放毛片 | 2019nv天堂香蕉在线观看 | 无码吃奶揉捏奶头高潮视频 | 亚洲中文字幕乱码av波多ji | 成人女人看片免费视频放人 | 中文字幕无码av波多野吉衣 | 宝宝好涨水快流出来免费视频 | 国产亚洲欧美日韩亚洲中文色 | 水蜜桃亚洲一二三四在线 | 国产97在线 | 亚洲 | 精品国产aⅴ无码一区二区 | 久久久久人妻一区精品色欧美 | 99久久婷婷国产综合精品青草免费 | 丰满人妻一区二区三区免费视频 | 人人妻人人澡人人爽欧美一区 | 中文字幕日韩精品一区二区三区 | 亚洲综合精品香蕉久久网 | 四虎国产精品一区二区 | 国产深夜福利视频在线 | 无码帝国www无码专区色综合 | 久久成人a毛片免费观看网站 | 成人女人看片免费视频放人 | 久久精品国产99久久6动漫 | 日日躁夜夜躁狠狠躁 | 久久久久久久女国产乱让韩 | 亚洲国产av美女网站 | 成人无码精品1区2区3区免费看 | 亚洲精品午夜国产va久久成人 | 5858s亚洲色大成网站www | 无码国产色欲xxxxx视频 | 国产超碰人人爽人人做人人添 | 国产乱子伦视频在线播放 | 香港三级日本三级妇三级 | 日本肉体xxxx裸交 | 中文无码精品a∨在线观看不卡 | 国产精品无码一区二区桃花视频 | 老司机亚洲精品影院 | 国产香蕉尹人视频在线 | 中文字幕乱码中文乱码51精品 | 97资源共享在线视频 | 丰满少妇高潮惨叫视频 | 东京热一精品无码av | 国产人妻精品一区二区三区不卡 | 爱做久久久久久 | 欧美精品一区二区精品久久 | 国产免费无码一区二区视频 | 少妇性荡欲午夜性开放视频剧场 | 99久久精品午夜一区二区 | 熟妇人妻无码xxx视频 | 人妻aⅴ无码一区二区三区 | 丰满妇女强制高潮18xxxx | 国产成人精品三级麻豆 | 中国大陆精品视频xxxx | 狠狠色噜噜狠狠狠狠7777米奇 | 国产精品办公室沙发 | 美女极度色诱视频国产 | а天堂中文在线官网 | 好男人www社区 | 亚洲午夜久久久影院 | 精品少妇爆乳无码av无码专区 | 久热国产vs视频在线观看 | 欧美三级不卡在线观看 | 精品夜夜澡人妻无码av蜜桃 | 国产人妖乱国产精品人妖 | 乱人伦人妻中文字幕无码久久网 | 强奷人妻日本中文字幕 | 九一九色国产 | 国产免费久久久久久无码 | 亚洲综合无码久久精品综合 | 国产97人人超碰caoprom | 无码人妻av免费一区二区三区 | 亚洲国产精品一区二区第一页 | 亚洲伊人久久精品影院 | 女人和拘做爰正片视频 | 亚洲国产成人a精品不卡在线 | 99久久人妻精品免费二区 | 久久精品女人天堂av免费观看 | 18精品久久久无码午夜福利 | 中文字幕无码日韩欧毛 | 色综合久久久无码网中文 | 国产农村妇女高潮大叫 | 亚洲色大成网站www | 少妇性俱乐部纵欲狂欢电影 | 正在播放东北夫妻内射 | 国产做国产爱免费视频 | 中文字幕久久久久人妻 | 国产网红无码精品视频 | 久久久精品成人免费观看 | 性色av无码免费一区二区三区 | 亚洲国产午夜精品理论片 | 久久精品人人做人人综合 | 中文精品无码中文字幕无码专区 | 中文字幕无码视频专区 | 老子影院午夜伦不卡 | 久久视频在线观看精品 | 国产乱子伦视频在线播放 | 婷婷色婷婷开心五月四房播播 | 无码精品人妻一区二区三区av | 亚洲欧美国产精品专区久久 | 人妻有码中文字幕在线 | 西西人体www44rt大胆高清 | 日日夜夜撸啊撸 | 国产精品久久久一区二区三区 | 国产精品亚洲专区无码不卡 | 九九热爱视频精品 | 激情内射日本一区二区三区 | 中文字幕无线码 | 国产精品久久久久9999小说 | 理论片87福利理论电影 | 丝袜 中出 制服 人妻 美腿 | 人妻熟女一区 | 无码人妻少妇伦在线电影 | 亚洲欧美日韩成人高清在线一区 | 三级4级全黄60分钟 | 久久久久久九九精品久 | 国产精品福利视频导航 | 日本一区二区三区免费高清 | aⅴ亚洲 日韩 色 图网站 播放 | 99久久久国产精品无码免费 | 国产精品欧美成人 | 国产亚洲人成在线播放 | 欧美怡红院免费全部视频 | 又湿又紧又大又爽a视频国产 | 成人亚洲精品久久久久软件 | 国产在线一区二区三区四区五区 | 狠狠cao日日穞夜夜穞av | 久久综合给合久久狠狠狠97色 | 成 人 免费观看网站 | 未满小14洗澡无码视频网站 | 蜜桃视频插满18在线观看 | 无码国产色欲xxxxx视频 | 狂野欧美性猛交免费视频 | 青草青草久热国产精品 | 老子影院午夜伦不卡 | 日本一区二区更新不卡 | 性欧美熟妇videofreesex | 97se亚洲精品一区 | 亚洲国产精品成人久久蜜臀 | 大肉大捧一进一出好爽视频 | 亚洲精品综合一区二区三区在线 | 国产偷自视频区视频 | 99国产精品白浆在线观看免费 | 对白脏话肉麻粗话av | 一区二区三区乱码在线 | 欧洲 | 免费无码av一区二区 | 强奷人妻日本中文字幕 | 巨爆乳无码视频在线观看 | 婷婷色婷婷开心五月四房播播 | 国产综合在线观看 | 国产一区二区三区日韩精品 | 中文字幕无码免费久久9一区9 | 东京热一精品无码av | 中文无码精品a∨在线观看不卡 | 日日天干夜夜狠狠爱 | 在线播放免费人成毛片乱码 | 久久久久久九九精品久 | 久久精品国产一区二区三区肥胖 | 丰满少妇人妻久久久久久 | 婷婷丁香五月天综合东京热 | 国产精品亚洲五月天高清 | 国产口爆吞精在线视频 | 日日干夜夜干 | 久久99精品久久久久婷婷 | 人人妻人人澡人人爽人人精品 | 老子影院午夜伦不卡 | 国产亚av手机在线观看 | 久久久久成人精品免费播放动漫 | 鲁一鲁av2019在线 | 日本一本二本三区免费 | 国产精品国产自线拍免费软件 | 国产精品人妻一区二区三区四 | 2020久久超碰国产精品最新 | 日本一本二本三区免费 | 亚洲成av人综合在线观看 | 国产绳艺sm调教室论坛 | 2019午夜福利不卡片在线 | 亚洲天堂2017无码 | 成人免费视频视频在线观看 免费 | 日本精品人妻无码77777 天堂一区人妻无码 | 久久精品国产精品国产精品污 | 在教室伦流澡到高潮hnp视频 | 三上悠亚人妻中文字幕在线 | 18禁止看的免费污网站 | 亚洲人成影院在线无码按摩店 | 一本大道久久东京热无码av | 亚洲国产精品久久久天堂 | 男女猛烈xx00免费视频试看 | 青草视频在线播放 | aⅴ亚洲 日韩 色 图网站 播放 | 成年美女黄网站色大免费全看 | 亚洲大尺度无码无码专区 | 久久综合给合久久狠狠狠97色 | 18无码粉嫩小泬无套在线观看 | 国产成人综合色在线观看网站 | 成人精品一区二区三区中文字幕 | 少妇一晚三次一区二区三区 | 国产一精品一av一免费 | 麻豆蜜桃av蜜臀av色欲av | 色婷婷综合激情综在线播放 | 国产精品鲁鲁鲁 | 国产一区二区三区影院 | 日本高清一区免费中文视频 | 亚洲精品国偷拍自产在线观看蜜桃 | 蜜桃视频韩日免费播放 | 日韩精品久久久肉伦网站 | 国产综合色产在线精品 | 澳门永久av免费网站 | 国产精品无码成人午夜电影 | 国产精品对白交换视频 | 中文字幕人成乱码熟女app | 欧美日韩一区二区免费视频 | 在线播放亚洲第一字幕 | www国产精品内射老师 | 日韩欧美成人免费观看 | a国产一区二区免费入口 | 4hu四虎永久在线观看 | 色五月丁香五月综合五月 | 亚洲精品欧美二区三区中文字幕 | 国产色视频一区二区三区 | 美女张开腿让人桶 | 日韩人妻无码中文字幕视频 | 国产精品.xx视频.xxtv | 无遮挡国产高潮视频免费观看 | 疯狂三人交性欧美 | 人人爽人人澡人人人妻 | 又粗又大又硬又长又爽 | 无遮无挡爽爽免费视频 | www一区二区www免费 | 丰满妇女强制高潮18xxxx | 东北女人啪啪对白 | 久久午夜无码鲁丝片秋霞 | 老司机亚洲精品影院无码 | 国产精品18久久久久久麻辣 | 欧洲熟妇精品视频 | 午夜熟女插插xx免费视频 | 国产熟妇另类久久久久 | 在线а√天堂中文官网 | 人人爽人人澡人人人妻 | 欧美一区二区三区 | 玩弄中年熟妇正在播放 | 国产人妻人伦精品 | 国产成人无码av片在线观看不卡 | 老头边吃奶边弄进去呻吟 | 精品国产aⅴ无码一区二区 | 久久综合给合久久狠狠狠97色 | 国产精品亚洲一区二区三区喷水 | 亚欧洲精品在线视频免费观看 | 国精品人妻无码一区二区三区蜜柚 | 亚洲精品久久久久久一区二区 | 东京热无码av男人的天堂 | 国产成人一区二区三区在线观看 | 2019午夜福利不卡片在线 | 人妻少妇精品无码专区二区 | 亚洲色偷偷男人的天堂 | 亚无码乱人伦一区二区 | 99久久久无码国产精品免费 | 久久久久久av无码免费看大片 | 国内精品久久毛片一区二区 | 亚洲啪av永久无码精品放毛片 | 97se亚洲精品一区 | 欧美性生交活xxxxxdddd | 欧美丰满熟妇xxxx性ppx人交 | 网友自拍区视频精品 | 亚洲国产午夜精品理论片 | 曰本女人与公拘交酡免费视频 | 国产成人亚洲综合无码 | 俄罗斯老熟妇色xxxx | 亚洲欧洲中文日韩av乱码 | 成熟妇人a片免费看网站 | 亚洲狠狠色丁香婷婷综合 | 一本色道久久综合狠狠躁 | 国产精品久久久久久亚洲影视内衣 | 沈阳熟女露脸对白视频 | 成人一区二区免费视频 | 欧美日韩色另类综合 | 日日橹狠狠爱欧美视频 | 国产又粗又硬又大爽黄老大爷视 | 午夜性刺激在线视频免费 | 午夜性刺激在线视频免费 | v一区无码内射国产 | 国产精品久久久午夜夜伦鲁鲁 | 久久久久久久久蜜桃 | 成人无码影片精品久久久 | 欧美日韩色另类综合 | 成人免费视频视频在线观看 免费 | 中文字幕无码日韩专区 | 精品偷拍一区二区三区在线看 | 美女极度色诱视频国产 | 国产av人人夜夜澡人人爽麻豆 | 免费国产黄网站在线观看 | 久久精品中文闷骚内射 | 中文无码伦av中文字幕 | 内射白嫩少妇超碰 | 麻豆成人精品国产免费 | 精品人妻av区 | 日日碰狠狠躁久久躁蜜桃 | 狠狠色欧美亚洲狠狠色www | 精品亚洲成av人在线观看 | 亚洲国产精品毛片av不卡在线 | 欧美日本免费一区二区三区 | 成人无码视频免费播放 | 激情亚洲一区国产精品 | 国产内射老熟女aaaa | 国产内射老熟女aaaa | 岛国片人妻三上悠亚 | 日韩精品久久久肉伦网站 | 丰满肥臀大屁股熟妇激情视频 | 一本久道久久综合婷婷五月 | 日韩人妻少妇一区二区三区 | 装睡被陌生人摸出水好爽 | 免费男性肉肉影院 | 色一情一乱一伦一区二区三欧美 | 精品无人国产偷自产在线 | 亚洲精品鲁一鲁一区二区三区 | 国产一区二区不卡老阿姨 | 初尝人妻少妇中文字幕 | 亚洲精品成人av在线 | 亚洲精品成人福利网站 | 免费国产成人高清在线观看网站 | 久精品国产欧美亚洲色aⅴ大片 | 日韩精品a片一区二区三区妖精 | 国产超碰人人爽人人做人人添 | 欧美日韩在线亚洲综合国产人 | 97资源共享在线视频 | 老头边吃奶边弄进去呻吟 | 欧美日韩视频无码一区二区三 | 樱花草在线播放免费中文 | 成熟妇人a片免费看网站 | 国产精品内射视频免费 | 免费人成在线观看网站 | 日本欧美一区二区三区乱码 | 色综合视频一区二区三区 | 精品久久久久香蕉网 | 精品一区二区三区无码免费视频 | 国产三级精品三级男人的天堂 | 性欧美牲交在线视频 | 青青青爽视频在线观看 | 亚洲热妇无码av在线播放 | 国模大胆一区二区三区 | 国产午夜无码视频在线观看 | 人妻尝试又大又粗久久 | 国产 浪潮av性色四虎 | 秋霞特色aa大片 | 国产九九九九九九九a片 | 精品无码国产一区二区三区av | 77777熟女视频在线观看 а天堂中文在线官网 | 久久久久99精品国产片 | 久久久久久久久888 | 久久国产劲爆∧v内射 | 久久综合久久自在自线精品自 | 丰满岳乱妇在线观看中字无码 | 桃花色综合影院 | 国产成人无码区免费内射一片色欲 | 少妇高潮喷潮久久久影院 | 国产日产欧产精品精品app | 性开放的女人aaa片 | 装睡被陌生人摸出水好爽 | 国产精品嫩草久久久久 | 久久午夜无码鲁丝片午夜精品 | 一个人看的www免费视频在线观看 | 黑人粗大猛烈进出高潮视频 | 国内精品久久久久久中文字幕 | 精品无人国产偷自产在线 | 国产精品美女久久久网av | 国产精品va在线观看无码 | 日本www一道久久久免费榴莲 | 久久亚洲中文字幕无码 | 国产成人一区二区三区别 | 日韩欧美中文字幕公布 | 性啪啪chinese东北女人 | 精品国产一区av天美传媒 | 亚洲va中文字幕无码久久不卡 | 国产电影无码午夜在线播放 | 亚洲最大成人网站 | 日本一区二区三区免费播放 | 精品久久久久久人妻无码中文字幕 | 欧美变态另类xxxx | 久久久www成人免费毛片 | 日日碰狠狠丁香久燥 | 伊人色综合久久天天小片 | 亚洲一区av无码专区在线观看 | 国产成人一区二区三区在线观看 | 丁香花在线影院观看在线播放 | 日日鲁鲁鲁夜夜爽爽狠狠 | 久久人人97超碰a片精品 | 国产va免费精品观看 | 婷婷五月综合缴情在线视频 | 狠狠色噜噜狠狠狠狠7777米奇 | 国产熟女一区二区三区四区五区 | 国产成人综合美国十次 | 欧美性生交活xxxxxdddd | 久久久久亚洲精品中文字幕 | 无码纯肉视频在线观看 | 乱人伦人妻中文字幕无码久久网 | 亚洲国产欧美在线成人 | 小sao货水好多真紧h无码视频 | 国产亚洲精品久久久ai换 | 成人无码视频在线观看网站 | 国产亚洲精品久久久久久大师 | 国产午夜视频在线观看 | 成人免费视频在线观看 | 久久视频在线观看精品 | 国产97人人超碰caoprom | 一区二区三区高清视频一 | 国产精品无码永久免费888 | 少妇性l交大片 | 国产内射爽爽大片视频社区在线 | 又黄又爽又色的视频 | 日本精品人妻无码免费大全 | 无码任你躁久久久久久久 | 亚洲精品一区二区三区在线观看 | 国产九九九九九九九a片 | 国产人妻精品一区二区三区不卡 | 欧美阿v高清资源不卡在线播放 | 国产精品人妻一区二区三区四 | 亚洲爆乳无码专区 | 丰满人妻精品国产99aⅴ | 麻豆成人精品国产免费 | 六十路熟妇乱子伦 | 内射巨臀欧美在线视频 | 亚洲精品一区二区三区大桥未久 | 国产极品美女高潮无套在线观看 | 欧美激情一区二区三区成人 | 久久综合香蕉国产蜜臀av | 国产av人人夜夜澡人人爽麻豆 | 在线天堂新版最新版在线8 | 亚洲国产午夜精品理论片 | 国产肉丝袜在线观看 | 久久久久久a亚洲欧洲av冫 | 成人精品视频一区二区 | 少妇愉情理伦片bd | 伊人久久婷婷五月综合97色 | 免费乱码人妻系列无码专区 | 亚洲熟妇色xxxxx亚洲 | 亚洲精品国偷拍自产在线观看蜜桃 | 西西人体www44rt大胆高清 | 美女扒开屁股让男人桶 | 欧美日韩久久久精品a片 | 日本饥渴人妻欲求不满 | 国产乱人伦av在线无码 | 国产农村乱对白刺激视频 | 全黄性性激高免费视频 | 一本久道久久综合狠狠爱 | 国产成人一区二区三区别 | 欧美国产日韩久久mv | 天堂а√在线地址中文在线 | 色一情一乱一伦 | 成熟妇人a片免费看网站 | 高中生自慰www网站 | 97夜夜澡人人爽人人喊中国片 | 久久久久久av无码免费看大片 | 国产suv精品一区二区五 | 国产精品久久久一区二区三区 | 国产黑色丝袜在线播放 | 精品国产一区二区三区四区 | 波多野结衣高清一区二区三区 | 久久久亚洲欧洲日产国码αv | 国产精品久久精品三级 | 亚洲精品国产第一综合99久久 | 久久精品人人做人人综合试看 | 人人澡人人妻人人爽人人蜜桃 | 久久久久久久女国产乱让韩 | 丰腴饱满的极品熟妇 | 伦伦影院午夜理论片 | 亚洲天堂2017无码中文 | 精品人妻av区 | 国产两女互慰高潮视频在线观看 | 国产舌乚八伦偷品w中 | 欧美日本精品一区二区三区 | 国产精品丝袜黑色高跟鞋 | 久久综合色之久久综合 | 天下第一社区视频www日本 | 国产乱人伦app精品久久 国产在线无码精品电影网 国产国产精品人在线视 | 无码人妻丰满熟妇区毛片18 | 久久国产精品萌白酱免费 | 国产真实伦对白全集 | 亚洲大尺度无码无码专区 | 国产成人精品视频ⅴa片软件竹菊 | 久久精品国产一区二区三区 | 国产农村妇女aaaaa视频 撕开奶罩揉吮奶头视频 | 国产精品久久久 | 国产午夜亚洲精品不卡 | 婷婷丁香五月天综合东京热 | 欧美变态另类xxxx | 永久免费观看国产裸体美女 | 一本久道久久综合婷婷五月 | 久久综合色之久久综合 | 亚洲国产精品一区二区第一页 | 日本欧美一区二区三区乱码 | 99久久精品无码一区二区毛片 | 奇米综合四色77777久久 东京无码熟妇人妻av在线网址 | 亚洲国产精品久久人人爱 | 亚拍精品一区二区三区探花 | 无码国产激情在线观看 | 亚洲精品成a人在线观看 | 午夜嘿嘿嘿影院 | 麻豆国产丝袜白领秘书在线观看 | 狠狠色色综合网站 | 亚洲成在人网站无码天堂 | 日本大乳高潮视频在线观看 | 日欧一片内射va在线影院 | 国产人妖乱国产精品人妖 | 粗大的内捧猛烈进出视频 | 一个人看的www免费视频在线观看 | 亚洲天堂2017无码中文 | 六月丁香婷婷色狠狠久久 | 色 综合 欧美 亚洲 国产 | 99国产欧美久久久精品 | 在线亚洲高清揄拍自拍一品区 | 国产精品嫩草久久久久 | 99久久婷婷国产综合精品青草免费 | 亚洲成av人在线观看网址 | 亚洲国产综合无码一区 | 国产农村妇女aaaaa视频 撕开奶罩揉吮奶头视频 | 亚洲精品一区二区三区婷婷月 | 国产成人一区二区三区别 | 丰满人妻一区二区三区免费视频 | 97夜夜澡人人爽人人喊中国片 | 免费观看又污又黄的网站 | 色爱情人网站 | 4hu四虎永久在线观看 | 亚欧洲精品在线视频免费观看 | 欧美老妇交乱视频在线观看 | 熟妇激情内射com | 成人免费视频一区二区 | 国产福利视频一区二区 | 国产精品无码一区二区三区不卡 | 装睡被陌生人摸出水好爽 | 国产精品久久久 | 激情亚洲一区国产精品 | 少妇厨房愉情理9仑片视频 | 国内精品久久久久久中文字幕 | 免费无码肉片在线观看 | 成年美女黄网站色大免费全看 | 精品国产成人一区二区三区 | 欧洲vodafone精品性 | 国产精品va在线播放 | 国模大胆一区二区三区 | 精品熟女少妇av免费观看 | 国内少妇偷人精品视频 | 日韩亚洲欧美中文高清在线 | 色偷偷人人澡人人爽人人模 | 久久久久久a亚洲欧洲av冫 | 曰韩少妇内射免费播放 | 人妻天天爽夜夜爽一区二区 | 久久亚洲精品中文字幕无男同 | 亚洲国产欧美国产综合一区 | 免费无码一区二区三区蜜桃大 | 无码国产激情在线观看 | 亚洲一区二区三区无码久久 | 国产亚洲人成a在线v网站 | 日本一区二区三区免费播放 | 六月丁香婷婷色狠狠久久 | 精品偷拍一区二区三区在线看 | 亚洲爆乳大丰满无码专区 | 少妇的肉体aa片免费 | 呦交小u女精品视频 | 18禁黄网站男男禁片免费观看 | 国产一区二区三区四区五区加勒比 | 精品久久久无码人妻字幂 | 亚洲成a人片在线观看无码3d | 三上悠亚人妻中文字幕在线 | 激情五月综合色婷婷一区二区 | 搡女人真爽免费视频大全 | 日本大香伊一区二区三区 | 乱人伦中文视频在线观看 | 久久久久久国产精品无码下载 | 四虎4hu永久免费 | 人妻无码αv中文字幕久久琪琪布 | 亚洲日本va午夜在线电影 | 亚洲国产av精品一区二区蜜芽 | 亚洲а∨天堂久久精品2021 | 成 人 网 站国产免费观看 | 99久久久无码国产aaa精品 | 欧美野外疯狂做受xxxx高潮 | 天堂在线观看www | 午夜无码人妻av大片色欲 | 国产精品二区一区二区aⅴ污介绍 | 内射爽无广熟女亚洲 | 午夜丰满少妇性开放视频 | 国产精品久久久一区二区三区 | 国产舌乚八伦偷品w中 | 黑人粗大猛烈进出高潮视频 | 亚洲a无码综合a国产av中文 | 亚洲春色在线视频 | 色五月丁香五月综合五月 | 六十路熟妇乱子伦 | 国产美女精品一区二区三区 | 色情久久久av熟女人妻网站 | 国精产品一区二区三区 | 国产农村妇女aaaaa视频 撕开奶罩揉吮奶头视频 | 在线播放亚洲第一字幕 | 国产香蕉97碰碰久久人人 | 强奷人妻日本中文字幕 | 国产精品久久久久久无码 | 日本丰满熟妇videos | 青青久在线视频免费观看 | 丰满人妻精品国产99aⅴ | 亚洲人成影院在线无码按摩店 | 亚洲日本va午夜在线电影 | 中文字幕人成乱码熟女app | 精品乱码久久久久久久 | 人人爽人人澡人人人妻 | 少妇太爽了在线观看 | 婷婷五月综合激情中文字幕 | 福利一区二区三区视频在线观看 | 成人无码精品一区二区三区 | 欧美自拍另类欧美综合图片区 | 又紧又大又爽精品一区二区 | 无码毛片视频一区二区本码 | 国产suv精品一区二区五 | 中文字幕乱码人妻二区三区 | 在线欧美精品一区二区三区 | 欧美成人免费全部网站 | 丝袜美腿亚洲一区二区 | 午夜精品一区二区三区的区别 | 香蕉久久久久久av成人 | 久久午夜无码鲁丝片午夜精品 | 国产另类ts人妖一区二区 | 少妇性l交大片 | 亚洲精品国产品国语在线观看 | 中文字幕乱码人妻无码久久 | 俄罗斯老熟妇色xxxx | 国产精品高潮呻吟av久久4虎 | 亚洲熟熟妇xxxx | 18禁止看的免费污网站 | 国产精品亚洲一区二区三区喷水 | 久久99热只有频精品8 | 成人无码精品一区二区三区 | 在线视频网站www色 | 人人妻人人澡人人爽欧美一区九九 | 小sao货水好多真紧h无码视频 | 久久 国产 尿 小便 嘘嘘 | 亚欧洲精品在线视频免费观看 | 人妻尝试又大又粗久久 | 国产精品久久久久久亚洲毛片 | 亲嘴扒胸摸屁股激烈网站 | 日韩av无码中文无码电影 | 国产成人一区二区三区在线观看 | 国产高潮视频在线观看 | 国产又爽又黄又刺激的视频 | 一二三四社区在线中文视频 | 99精品视频在线观看免费 | 性欧美熟妇videofreesex | 午夜精品久久久久久久 | 天天综合网天天综合色 | 国产成人一区二区三区在线观看 | 成熟人妻av无码专区 | 日本在线高清不卡免费播放 | 无码乱肉视频免费大全合集 | 理论片87福利理论电影 | 国产精品国产三级国产专播 | 亚洲国产综合无码一区 | 欧美成人家庭影院 | 国产免费观看黄av片 | av人摸人人人澡人人超碰下载 | 欧美黑人巨大xxxxx | 亚洲精品一区二区三区在线观看 | 成人精品一区二区三区中文字幕 | 国产乱人无码伦av在线a | 国产内射爽爽大片视频社区在线 | 内射巨臀欧美在线视频 | 无套内谢老熟女 | 亚洲熟妇色xxxxx欧美老妇y | 久久久精品国产sm最大网站 | 国产高清不卡无码视频 | 亚洲成熟女人毛毛耸耸多 | 嫩b人妻精品一区二区三区 | 久久99久久99精品中文字幕 | 一本久久伊人热热精品中文字幕 | 精品欧洲av无码一区二区三区 | а√资源新版在线天堂 | 亚洲精品国产a久久久久久 | 4hu四虎永久在线观看 | 国产亚洲tv在线观看 | 国产乱人伦偷精品视频 | 久久精品女人天堂av免费观看 | 日韩亚洲欧美精品综合 | 天天躁夜夜躁狠狠是什么心态 | 成年美女黄网站色大免费视频 | 少妇性荡欲午夜性开放视频剧场 | 久久99精品国产麻豆 | aa片在线观看视频在线播放 | 亚洲成a人片在线观看无码3d | 无码福利日韩神码福利片 | 狂野欧美性猛xxxx乱大交 | 精品久久久中文字幕人妻 | 亚洲伊人久久精品影院 | 久久综合久久自在自线精品自 | 一本大道久久东京热无码av | 国产精品18久久久久久麻辣 | 国产av久久久久精东av | 国产明星裸体无码xxxx视频 | 18无码粉嫩小泬无套在线观看 | 色妞www精品免费视频 | 97无码免费人妻超级碰碰夜夜 | 四十如虎的丰满熟妇啪啪 | 色诱久久久久综合网ywww | 国产成人人人97超碰超爽8 | 扒开双腿吃奶呻吟做受视频 | 亚洲人成影院在线无码按摩店 | 理论片87福利理论电影 | 亚洲人成影院在线无码按摩店 | 国产精品久久久久久久影院 | 国产乱人伦app精品久久 国产在线无码精品电影网 国产国产精品人在线视 | 极品尤物被啪到呻吟喷水 | 少妇无码一区二区二三区 | 水蜜桃av无码 | 台湾无码一区二区 | 久久亚洲中文字幕精品一区 | 久久久久久久女国产乱让韩 | 在教室伦流澡到高潮hnp视频 | 九九综合va免费看 | 狠狠cao日日穞夜夜穞av | 国产成人一区二区三区在线观看 | 亚洲日韩av一区二区三区四区 | 偷窥日本少妇撒尿chinese | 亚洲自偷自偷在线制服 | 午夜精品久久久久久久 | 狠狠躁日日躁夜夜躁2020 | 女人被爽到呻吟gif动态图视看 | 欧美激情综合亚洲一二区 | 无码人妻丰满熟妇区毛片18 | 国内精品久久久久久中文字幕 | 国产一区二区三区影院 | 妺妺窝人体色www婷婷 | 国产亚洲精品精品国产亚洲综合 | 精品国精品国产自在久国产87 | 亚洲国产精品久久久久久 | 天下第一社区视频www日本 | 奇米影视7777久久精品人人爽 | 国产亚洲精品久久久ai换 | 亚洲欧美日韩成人高清在线一区 | 国产乱人伦av在线无码 | 欧美人与牲动交xxxx | 久久这里只有精品视频9 | 波多野42部无码喷潮在线 | 国产两女互慰高潮视频在线观看 | 蜜臀aⅴ国产精品久久久国产老师 | 人妻少妇精品无码专区二区 | 国模大胆一区二区三区 | 午夜精品久久久内射近拍高清 | 国产av一区二区三区最新精品 | 亚洲精品欧美二区三区中文字幕 | 免费乱码人妻系列无码专区 | 亚洲熟悉妇女xxx妇女av | 中文精品无码中文字幕无码专区 | 久久综合网欧美色妞网 | 日本精品久久久久中文字幕 | 无套内谢老熟女 | 国产成人无码av片在线观看不卡 | 樱花草在线社区www | 国内少妇偷人精品视频 | 国产婷婷色一区二区三区在线 | 亚洲综合伊人久久大杳蕉 | a片在线免费观看 | 女人色极品影院 | 综合人妻久久一区二区精品 | 内射欧美老妇wbb | 久久zyz资源站无码中文动漫 | 国产明星裸体无码xxxx视频 | 窝窝午夜理论片影院 | 日本一卡2卡3卡4卡无卡免费网站 国产一区二区三区影院 | 亚洲码国产精品高潮在线 | 国产办公室秘书无码精品99 | 国产精品久久国产精品99 | 国产亚洲精品久久久久久国模美 | 377p欧洲日本亚洲大胆 | 丝袜美腿亚洲一区二区 | 婷婷色婷婷开心五月四房播播 | 国产精品久久精品三级 | 亚洲精品成人福利网站 | 亚洲精品午夜国产va久久成人 | 国产人妻久久精品二区三区老狼 | 无码av中文字幕免费放 | 色窝窝无码一区二区三区色欲 | 无码人妻丰满熟妇区五十路百度 | 亚洲成a人一区二区三区 | 欧美成人午夜精品久久久 | 97夜夜澡人人爽人人喊中国片 | 中文字幕人成乱码熟女app | 福利一区二区三区视频在线观看 | 成人片黄网站色大片免费观看 | 国产成人无码av片在线观看不卡 | 无码精品人妻一区二区三区av | 亚洲日韩乱码中文无码蜜桃臀网站 | 亚洲国产一区二区三区在线观看 | 午夜男女很黄的视频 | 欧美日韩色另类综合 | 高清无码午夜福利视频 | 精品人人妻人人澡人人爽人人 | 成人无码精品1区2区3区免费看 | 国产欧美熟妇另类久久久 | 亚洲第一网站男人都懂 | 国产成人一区二区三区别 | 欧美老人巨大xxxx做受 | 无遮挡啪啪摇乳动态图 | 动漫av网站免费观看 | 夜夜高潮次次欢爽av女 | 久久久久久久久888 | 日本丰满护士爆乳xxxx | 亚洲欧美国产精品专区久久 | 高潮毛片无遮挡高清免费视频 | 又湿又紧又大又爽a视频国产 | 图片小说视频一区二区 | 午夜无码区在线观看 | 2020久久超碰国产精品最新 | 国产精品久久久 | 亚洲精品综合一区二区三区在线 | 漂亮人妻洗澡被公强 日日躁 | 国内精品人妻无码久久久影院蜜桃 | 色欲久久久天天天综合网精品 | 久久久中文字幕日本无吗 | 成 人 网 站国产免费观看 | а√资源新版在线天堂 | 麻豆国产97在线 | 欧洲 | 国产sm调教视频在线观看 | 图片小说视频一区二区 | 18精品久久久无码午夜福利 | 无套内谢的新婚少妇国语播放 | 日本大乳高潮视频在线观看 | 中文字幕乱码亚洲无线三区 | 撕开奶罩揉吮奶头视频 | 性欧美大战久久久久久久 | 欧洲欧美人成视频在线 | 国产精品第一区揄拍无码 | 97色伦图片97综合影院 | 国产性生大片免费观看性 | 国产真实乱对白精彩久久 | 精品无码一区二区三区爱欲 | 乌克兰少妇xxxx做受 | 又大又黄又粗又爽的免费视频 | 国产 精品 自在自线 | 欧美日韩精品 | 久久99精品国产麻豆蜜芽 | 999久久久国产精品消防器材 | 熟妇人妻激情偷爽文 | 日本在线高清不卡免费播放 | 思思久久99热只有频精品66 | 久久精品中文闷骚内射 | 麻豆国产97在线 | 欧洲 | 鲁一鲁av2019在线 | 免费无码的av片在线观看 | 亚洲精品成a人在线观看 | 精品无人区无码乱码毛片国产 | 国产偷自视频区视频 | 色欲综合久久中文字幕网 | 丝袜 中出 制服 人妻 美腿 | 55夜色66夜色国产精品视频 | 窝窝午夜理论片影院 | 国产办公室秘书无码精品99 | 六月丁香婷婷色狠狠久久 | 亚洲精品午夜无码电影网 | 国产在热线精品视频 | 色婷婷av一区二区三区之红樱桃 | 一本久道久久综合婷婷五月 | 久久亚洲中文字幕精品一区 | 国产人妻精品午夜福利免费 | a片免费视频在线观看 | 女人高潮内射99精品 | 精品午夜福利在线观看 | 精品少妇爆乳无码av无码专区 | 久久人人爽人人爽人人片av高清 | 老司机亚洲精品影院 | 亚洲午夜久久久影院 | 国产精品多人p群无码 | 日本免费一区二区三区最新 | 夫妻免费无码v看片 | 国产欧美精品一区二区三区 | www国产亚洲精品久久久日本 | 亚洲爆乳大丰满无码专区 | 性啪啪chinese东北女人 | 国产精品无套呻吟在线 | 曰韩无码二三区中文字幕 | 国产精品久久国产三级国 | 亚无码乱人伦一区二区 | 亚洲一区二区三区国产精华液 | 无码人妻黑人中文字幕 | 国产av无码专区亚洲awww | 伊人久久大香线焦av综合影院 | 99久久亚洲精品无码毛片 | 亚洲国产精品美女久久久久 | 一二三四在线观看免费视频 | 欧美精品一区二区精品久久 | 天下第一社区视频www日本 | 国产精品无码成人午夜电影 | 国产精品国产自线拍免费软件 | 久久精品国产一区二区三区肥胖 | 西西人体www44rt大胆高清 | 久久成人a毛片免费观看网站 | 亚洲色成人中文字幕网站 | 亚洲人成人无码网www国产 | 成人片黄网站色大片免费观看 | 97无码免费人妻超级碰碰夜夜 | 成年美女黄网站色大免费全看 | 少妇久久久久久人妻无码 | 国精品人妻无码一区二区三区蜜柚 | 亚洲色欲色欲欲www在线 | 牲欲强的熟妇农村老妇女视频 | 无码人妻久久一区二区三区不卡 | 国产九九九九九九九a片 | 国产精品美女久久久网av | 国产 精品 自在自线 | 在线观看国产一区二区三区 | 国产猛烈高潮尖叫视频免费 | 婷婷丁香六月激情综合啪 | 综合激情五月综合激情五月激情1 | 无码吃奶揉捏奶头高潮视频 | 波多野结衣av一区二区全免费观看 | 日本va欧美va欧美va精品 | 国产色xx群视频射精 | 亚洲欧美中文字幕5发布 | 国产精品久久久午夜夜伦鲁鲁 | 高中生自慰www网站 | 两性色午夜免费视频 | 色欲人妻aaaaaaa无码 | 无码播放一区二区三区 | 色情久久久av熟女人妻网站 | 扒开双腿疯狂进出爽爽爽视频 | 久久精品女人天堂av免费观看 | 免费看男女做好爽好硬视频 | 秋霞成人午夜鲁丝一区二区三区 | 国产成人午夜福利在线播放 | 一本久久伊人热热精品中文字幕 | 国产另类ts人妖一区二区 | 国产电影无码午夜在线播放 | 日本丰满护士爆乳xxxx | 午夜精品久久久久久久久 | 亚洲日韩一区二区三区 | 日韩亚洲欧美中文高清在线 | 99久久久无码国产精品免费 | 久久人人爽人人爽人人片ⅴ | 久久国产精品萌白酱免费 | 97色伦图片97综合影院 | 日本xxxx色视频在线观看免费 | 四十如虎的丰满熟妇啪啪 | 日本一区二区三区免费高清 | 国产无遮挡吃胸膜奶免费看 | 久久zyz资源站无码中文动漫 | 国产艳妇av在线观看果冻传媒 | 午夜时刻免费入口 | 无码播放一区二区三区 | 久久精品国产精品国产精品污 | 人妻有码中文字幕在线 | 久久久精品国产sm最大网站 | 亚洲成av人在线观看网址 | 日日碰狠狠丁香久燥 | 亚洲aⅴ无码成人网站国产app | 国内少妇偷人精品视频免费 | 日本熟妇大屁股人妻 | 免费乱码人妻系列无码专区 | 少妇太爽了在线观看 | 亚洲成熟女人毛毛耸耸多 | 亚洲中文无码av永久不收费 | 亚洲精品国产第一综合99久久 | 澳门永久av免费网站 | 亚洲区小说区激情区图片区 | 亚洲精品成人福利网站 | 精品国精品国产自在久国产87 | 国产精品理论片在线观看 | a国产一区二区免费入口 | 国产精品视频免费播放 | 2020久久超碰国产精品最新 | 婷婷五月综合激情中文字幕 | 国产性生大片免费观看性 | 日韩av无码一区二区三区不卡 | 亚洲色在线无码国产精品不卡 | 好男人www社区 | 国产午夜福利亚洲第一 | 四虎影视成人永久免费观看视频 | 久久国产精品偷任你爽任你 | 夜夜躁日日躁狠狠久久av | 午夜男女很黄的视频 | 久久精品中文字幕一区 | 亚洲熟妇色xxxxx欧美老妇 | 国产精品久久久久9999小说 | 国产97色在线 | 免 | 国产精品久久久一区二区三区 | 天干天干啦夜天干天2017 | 性生交大片免费看l | 色欲人妻aaaaaaa无码 | 日本va欧美va欧美va精品 | 伊人久久大香线蕉午夜 | 乱码午夜-极国产极内射 | 婷婷丁香六月激情综合啪 | аⅴ资源天堂资源库在线 | 蜜桃av抽搐高潮一区二区 | 一区二区三区乱码在线 | 欧洲 | 最新版天堂资源中文官网 | 婷婷综合久久中文字幕蜜桃三电影 | 欧美成人家庭影院 | 人人爽人人爽人人片av亚洲 | 亚洲精品成人av在线 | 久久久av男人的天堂 | 国产成人精品一区二区在线小狼 | 色五月丁香五月综合五月 | 日日橹狠狠爱欧美视频 | 欧美变态另类xxxx | 亚洲一区二区三区香蕉 | 久9re热视频这里只有精品 | 给我免费的视频在线观看 | 蜜桃视频插满18在线观看 | 国产后入清纯学生妹 | 精品一区二区不卡无码av | 帮老师解开蕾丝奶罩吸乳网站 | 无套内谢的新婚少妇国语播放 | 熟妇人妻无乱码中文字幕 | 沈阳熟女露脸对白视频 | 国产内射爽爽大片视频社区在线 | 亚洲va中文字幕无码久久不卡 | 国内揄拍国内精品人妻 | 久久午夜无码鲁丝片秋霞 | 国产猛烈高潮尖叫视频免费 | 久久久久人妻一区精品色欧美 | 国精产品一区二区三区 | 精品国产av色一区二区深夜久久 | 在线欧美精品一区二区三区 | 亚洲中文字幕无码一久久区 | 亚洲精品国产a久久久久久 | 学生妹亚洲一区二区 | 一本大道久久东京热无码av | 人妻无码久久精品人妻 | 最近免费中文字幕中文高清百度 | 蜜桃av蜜臀av色欲av麻 999久久久国产精品消防器材 | 国产特级毛片aaaaaa高潮流水 | 精品无码一区二区三区爱欲 | 人妻无码αv中文字幕久久琪琪布 | 少妇久久久久久人妻无码 | 国产乱人偷精品人妻a片 | 人妻少妇精品久久 | 亚洲一区二区三区无码久久 | 亚拍精品一区二区三区探花 | 欧美人与善在线com | 成人aaa片一区国产精品 | 老熟妇仑乱视频一区二区 | 欧美国产日韩亚洲中文 | 丁香花在线影院观看在线播放 | 无码一区二区三区在线观看 | 国模大胆一区二区三区 | 九月婷婷人人澡人人添人人爽 | 亚洲国产成人av在线观看 | 奇米综合四色77777久久 东京无码熟妇人妻av在线网址 | 日日碰狠狠躁久久躁蜜桃 | аⅴ资源天堂资源库在线 | 国产av久久久久精东av | 精品少妇爆乳无码av无码专区 | 少妇无码av无码专区在线观看 | 一本久久a久久精品vr综合 | 国产国产精品人在线视 | 中文字幕人成乱码熟女app | 欧美日本免费一区二区三区 | 98国产精品综合一区二区三区 | 成人无码精品一区二区三区 | 精品久久久中文字幕人妻 | 99久久精品午夜一区二区 | 麻豆md0077饥渴少妇 | 欧美xxxxx精品 | 学生妹亚洲一区二区 | 欧美国产日韩久久mv | 纯爱无遮挡h肉动漫在线播放 | 老子影院午夜精品无码 | 狠狠综合久久久久综合网 | 亚洲色欲色欲天天天www | 国产两女互慰高潮视频在线观看 | 亚洲区小说区激情区图片区 | 国产成人一区二区三区在线观看 | 99久久99久久免费精品蜜桃 | 亚洲人成网站免费播放 | 国产精品无码一区二区桃花视频 | 天干天干啦夜天干天2017 | 成人精品视频一区二区三区尤物 | 欧美成人高清在线播放 | 少女韩国电视剧在线观看完整 | 亚洲国精产品一二二线 | 中文字幕 人妻熟女 | 成人一在线视频日韩国产 | 国产亚洲精品久久久久久大师 | 九九久久精品国产免费看小说 | 久久无码中文字幕免费影院蜜桃 | 亚洲 高清 成人 动漫 | 色综合久久久久综合一本到桃花网 | 99久久人妻精品免费二区 | 精品成人av一区二区三区 | 欧美 丝袜 自拍 制服 另类 | 精品厕所偷拍各类美女tp嘘嘘 | 成在人线av无码免费 | 久久综合久久自在自线精品自 | 麻豆蜜桃av蜜臀av色欲av | 欧美人与禽猛交狂配 | 国产精品手机免费 | 国产精品人妻一区二区三区四 | 啦啦啦www在线观看免费视频 | 1000部夫妻午夜免费 | 日韩成人一区二区三区在线观看 | 99久久亚洲精品无码毛片 | 欧美国产日韩久久mv | 又粗又大又硬又长又爽 | 精品国产一区二区三区四区在线看 | 亚洲热妇无码av在线播放 | 性做久久久久久久久 | 国产成人无码av片在线观看不卡 | 97久久精品无码一区二区 | 中文字幕日韩精品一区二区三区 | 国精产品一品二品国精品69xx | 青草青草久热国产精品 | 老头边吃奶边弄进去呻吟 | 丰满妇女强制高潮18xxxx | 久久久亚洲欧洲日产国码αv | 亚洲另类伦春色综合小说 | 97夜夜澡人人双人人人喊 | 内射老妇bbwx0c0ck | 麻豆精品国产精华精华液好用吗 | 台湾无码一区二区 | 强开小婷嫩苞又嫩又紧视频 | 久久久婷婷五月亚洲97号色 | 欧美成人家庭影院 | 俄罗斯老熟妇色xxxx | 国产又粗又硬又大爽黄老大爷视 | 国产一区二区三区日韩精品 | 精品成人av一区二区三区 | 曰本女人与公拘交酡免费视频 | 精品国产一区二区三区四区在线看 | 成人一在线视频日韩国产 | 国产精品久久久av久久久 | 午夜福利试看120秒体验区 | 久久无码中文字幕免费影院蜜桃 | 97夜夜澡人人爽人人喊中国片 | 成人一区二区免费视频 | 中文字幕日产无线码一区 | 欧美自拍另类欧美综合图片区 | 亚洲成熟女人毛毛耸耸多 | 久久久久人妻一区精品色欧美 | 亚洲日韩av片在线观看 | 偷窥日本少妇撒尿chinese | 国产口爆吞精在线视频 | 1000部啪啪未满十八勿入下载 | 国产午夜精品一区二区三区嫩草 | 综合激情五月综合激情五月激情1 | 国产一区二区三区影院 | 国产精品高潮呻吟av久久 | 巨爆乳无码视频在线观看 | 国产两女互慰高潮视频在线观看 | 国产精品沙发午睡系列 | 精品久久8x国产免费观看 | 色欲久久久天天天综合网精品 | 精品偷自拍另类在线观看 | 无遮挡国产高潮视频免费观看 | 久久精品国产亚洲精品 | 中文无码伦av中文字幕 | 在线欧美精品一区二区三区 | 国产人妻久久精品二区三区老狼 | 精品夜夜澡人妻无码av蜜桃 | 青青草原综合久久大伊人精品 | 麻豆果冻传媒2021精品传媒一区下载 | 精品国产一区二区三区四区 | 少妇性荡欲午夜性开放视频剧场 | 国产va免费精品观看 | 日日摸天天摸爽爽狠狠97 | 色窝窝无码一区二区三区色欲 | 国产性生大片免费观看性 | 国产香蕉97碰碰久久人人 | a国产一区二区免费入口 | 黑人玩弄人妻中文在线 | 兔费看少妇性l交大片免费 | 亚洲成色在线综合网站 | 在线看片无码永久免费视频 | 国产一区二区不卡老阿姨 | 国产尤物精品视频 | 久久国产精品_国产精品 | 内射欧美老妇wbb | 亚洲经典千人经典日产 | 日本一卡2卡3卡4卡无卡免费网站 国产一区二区三区影院 | 亚洲一区二区三区含羞草 | 美女黄网站人色视频免费国产 | 领导边摸边吃奶边做爽在线观看 | 国产婷婷色一区二区三区在线 | 亚洲国产av精品一区二区蜜芽 | 国产内射爽爽大片视频社区在线 | 亚洲精品成a人在线观看 | 色综合视频一区二区三区 | 女人高潮内射99精品 | 亚洲一区二区三区偷拍女厕 | 国产精品亚洲五月天高清 | 中文无码精品a∨在线观看不卡 | 国产亚洲tv在线观看 | 人妻少妇精品无码专区二区 | 国产内射老熟女aaaa | 中文字幕亚洲情99在线 | 国产成人无码av在线影院 | 亚洲爆乳大丰满无码专区 | 无码人妻出轨黑人中文字幕 | 国产疯狂伦交大片 | 久久久精品人妻久久影视 | 日日碰狠狠躁久久躁蜜桃 | 中文字幕乱码人妻二区三区 | 丰满人妻精品国产99aⅴ | 精品偷拍一区二区三区在线看 | 高中生自慰www网站 | 精品无人国产偷自产在线 | 国产精品美女久久久 | 亚洲精品中文字幕乱码 | 国产疯狂伦交大片 | 日日碰狠狠躁久久躁蜜桃 | 自拍偷自拍亚洲精品被多人伦好爽 | 影音先锋中文字幕无码 | 任你躁在线精品免费 | 亚洲人亚洲人成电影网站色 | 精品国精品国产自在久国产87 | 精品乱码久久久久久久 | 一本久道久久综合婷婷五月 | 桃花色综合影院 | 波多野结衣乳巨码无在线观看 | 一区二区三区高清视频一 | 久久久久亚洲精品中文字幕 | 久久国产36精品色熟妇 | 久久精品女人天堂av免费观看 | 国产人妻精品午夜福利免费 | 亚洲熟妇色xxxxx欧美老妇y | 成人免费视频视频在线观看 免费 | 男人扒开女人内裤强吻桶进去 | 一本久道久久综合婷婷五月 | 国产成人精品必看 | 色欲久久久天天天综合网精品 | 精品久久8x国产免费观看 | 无人区乱码一区二区三区 | 又大又紧又粉嫩18p少妇 | 国产黄在线观看免费观看不卡 | 天海翼激烈高潮到腰振不止 | 狠狠综合久久久久综合网 | 无遮挡国产高潮视频免费观看 | 国产午夜精品一区二区三区嫩草 | 一本精品99久久精品77 | 中文毛片无遮挡高清免费 | 免费乱码人妻系列无码专区 | 精品偷拍一区二区三区在线看 | 亚洲小说春色综合另类 | 国产亚洲精品久久久久久国模美 | 老熟女重囗味hdxx69 | 性欧美牲交在线视频 | 色一情一乱一伦 | 日韩人妻无码一区二区三区久久99 | 国产熟妇高潮叫床视频播放 | 亚洲午夜久久久影院 | 丰满护士巨好爽好大乳 | 久久久久久av无码免费看大片 | 麻豆av传媒蜜桃天美传媒 | 亚洲日本va中文字幕 | 久久亚洲中文字幕无码 | 亚洲色欲色欲天天天www | 男人和女人高潮免费网站 | 熟妇激情内射com | 国产精品高潮呻吟av久久4虎 | 国精品人妻无码一区二区三区蜜柚 | 大肉大捧一进一出视频出来呀 | 国内揄拍国内精品人妻 | 成人一区二区免费视频 | 天堂а√在线地址中文在线 | 日日橹狠狠爱欧美视频 | 中文字幕乱码中文乱码51精品 | 曰韩无码二三区中文字幕 | 无码帝国www无码专区色综合 | 中文字幕人成乱码熟女app | 水蜜桃色314在线观看 | 亚洲欧美精品伊人久久 | 中文字幕人妻无码一夲道 | 国产两女互慰高潮视频在线观看 | 夜夜躁日日躁狠狠久久av | 又大又黄又粗又爽的免费视频 | 欧美一区二区三区 | 日本va欧美va欧美va精品 | 99久久精品无码一区二区毛片 | 高潮喷水的毛片 | www一区二区www免费 | 扒开双腿吃奶呻吟做受视频 | 九一九色国产 | 国产精品亚洲一区二区三区喷水 | 中国女人内谢69xxxxxa片 | 亚洲七七久久桃花影院 | 精品国偷自产在线 | 免费观看又污又黄的网站 | 久久精品中文闷骚内射 | 国产精品久久久一区二区三区 | 牲交欧美兽交欧美 | 亚洲一区二区三区无码久久 | 欧美放荡的少妇 | 在线看片无码永久免费视频 | 狠狠色色综合网站 | 在线视频网站www色 | 色偷偷av老熟女 久久精品人妻少妇一区二区三区 | 国内精品九九久久久精品 | 无码人妻丰满熟妇区五十路百度 | 欧美怡红院免费全部视频 | 中文字幕av日韩精品一区二区 | 精品乱子伦一区二区三区 | 日日麻批免费40分钟无码 | 丝袜美腿亚洲一区二区 | 久精品国产欧美亚洲色aⅴ大片 | 一区二区三区乱码在线 | 欧洲 | 一本色道久久综合狠狠躁 | av无码电影一区二区三区 | 精品久久久久久人妻无码中文字幕 | 免费看少妇作爱视频 | 色五月五月丁香亚洲综合网 | 麻豆国产丝袜白领秘书在线观看 | 粗大的内捧猛烈进出视频 | 国产成人无码午夜视频在线观看 | 成人影院yy111111在线观看 | 亚洲精品中文字幕久久久久 | 老太婆性杂交欧美肥老太 | 天天拍夜夜添久久精品 | 亚洲国产一区二区三区在线观看 | 亚洲日韩一区二区三区 | 天海翼激烈高潮到腰振不止 | 日日鲁鲁鲁夜夜爽爽狠狠 | 中文字幕无码免费久久9一区9 | 国产高潮视频在线观看 | 乱码av麻豆丝袜熟女系列 | 影音先锋中文字幕无码 | 亚洲精品成人av在线 | 99久久人妻精品免费二区 | 欧美变态另类xxxx | 日产精品高潮呻吟av久久 | 少妇高潮喷潮久久久影院 | 丰满少妇高潮惨叫视频 | 无码毛片视频一区二区本码 | 国产精华av午夜在线观看 | 美女黄网站人色视频免费国产 | 亚洲理论电影在线观看 | 久久久久亚洲精品中文字幕 | 小sao货水好多真紧h无码视频 | 高潮毛片无遮挡高清免费视频 | 成年女人永久免费看片 | 精品久久久久久亚洲精品 | 亚洲s色大片在线观看 | 日韩欧美中文字幕在线三区 | 亚洲欧美日韩国产精品一区二区 | 国产精品资源一区二区 | 精品国产精品久久一区免费式 | 日韩少妇内射免费播放 | 色婷婷av一区二区三区之红樱桃 | 国产精品高潮呻吟av久久 | 无遮无挡爽爽免费视频 | 日韩av无码一区二区三区不卡 | 国产成人无码一二三区视频 | 久久久婷婷五月亚洲97号色 | 老熟妇仑乱视频一区二区 | 精品久久久久香蕉网 | 久久99精品久久久久久动态图 | 乌克兰少妇性做爰 | 精品一区二区三区波多野结衣 | 精品国产青草久久久久福利 | 天海翼激烈高潮到腰振不止 | 四十如虎的丰满熟妇啪啪 | 西西人体www44rt大胆高清 | 成人性做爰aaa片免费看不忠 | 荡女精品导航 | 国产精品无码成人午夜电影 | 久久亚洲日韩精品一区二区三区 | 熟妇女人妻丰满少妇中文字幕 | 六十路熟妇乱子伦 | 国产一区二区三区日韩精品 | 人人澡人人妻人人爽人人蜜桃 | 狠狠色丁香久久婷婷综合五月 | 秋霞成人午夜鲁丝一区二区三区 | 欧美高清在线精品一区 | 成在人线av无码免观看麻豆 | 国产亚洲日韩欧美另类第八页 | 亚洲欧美综合区丁香五月小说 | 国产成人一区二区三区别 | 三上悠亚人妻中文字幕在线 | 日韩欧美成人免费观看 | 亚洲成色在线综合网站 | 黑人巨大精品欧美黑寡妇 | 久久99精品国产.久久久久 | 奇米影视7777久久精品人人爽 | 1000部啪啪未满十八勿入下载 | 无遮挡国产高潮视频免费观看 | 国产av人人夜夜澡人人爽麻豆 | 国产成人久久精品流白浆 | 亚洲熟妇色xxxxx欧美老妇 | 人人澡人人妻人人爽人人蜜桃 | 亚洲中文字幕av在天堂 | 久久zyz资源站无码中文动漫 | 婷婷综合久久中文字幕蜜桃三电影 | 亚洲精品国产精品乱码视色 | av人摸人人人澡人人超碰下载 | 亚洲日本一区二区三区在线 | 中文字幕中文有码在线 | 岛国片人妻三上悠亚 | 国产免费观看黄av片 | 狂野欧美性猛交免费视频 | 熟女体下毛毛黑森林 | 99re在线播放 | 欧洲欧美人成视频在线 | 免费无码av一区二区 | 成人动漫在线观看 | 性色欲网站人妻丰满中文久久不卡 | 国产香蕉尹人视频在线 | 少妇性l交大片欧洲热妇乱xxx | 亚洲 另类 在线 欧美 制服 | 人人妻人人澡人人爽人人精品 | yw尤物av无码国产在线观看 | 日本精品人妻无码免费大全 | 久久久精品欧美一区二区免费 | 国产人妻久久精品二区三区老狼 | 台湾无码一区二区 | 久久精品一区二区三区四区 | 久久精品99久久香蕉国产色戒 | 两性色午夜视频免费播放 | 国产精品资源一区二区 | 在线播放免费人成毛片乱码 | 国产午夜手机精彩视频 | 国产成人精品三级麻豆 | 福利一区二区三区视频在线观看 | 在线精品亚洲一区二区 | 亚洲欧美国产精品专区久久 | 久久久精品人妻久久影视 |