摘要:第章將詳細討論瀏覽器對象模型將構造函數當作函數構造函數與其他函數的唯一區別就在于調用方式不同。而在構造函數內部,我們將屬性設置成等于全局的函數。默認情況下,所有原型對象都會自動獲得一個構造函數屬性,這個屬性是一個指向屬性所在函數的指針。
面向對象(Object-Oriented, OO)的語言有一個標志,它們都有類的概念,而通過類可以創建任意多個具有相同屬性和方法的對象。
ECMAScript中沒有類的概念,因此它的對象也與基類的語言中的對象有所不同
ECMA-262把對象定義為:“無序屬性的集合,其屬性可以包含基本值、對象或者函數。” 我們可以把ECMAScript的對象想象成散列表:無非就是一組名值對,其中值可以是數據或者函數
每個對象都是基于一個引用類型創建的,這個引用類型可以是第5章討論的原生類型,也可以是開發者定義的類型
理解對象// 創建對象,賦給屬性 var person = new Object(); person.name = "Nicholas"; person.age = 29; person.job = "Sofware Engineer"; person.sayName = function() { alert(this.name); } // 字面量方式創建對象 var person = { name: "Nicholas", age: 29, job: "Sofware Engineer", sayName: function() { alert(this.name); } }屬性類型
ECMAScript中有兩種屬性:數據屬性和訪問器屬性
數據屬性
數據屬性包含一個數值的位置。在這個位置可以讀取和寫入值。數據屬性有4個描述其行為的特性
[[Configurable]] 表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為訪問器屬性。像前述例子中那樣直接在對象定義的屬性,它們這個特性默認值為true
[[Enumerable]] 表示能否通過for-in循環返回屬性。前述例子這個特性默認為true
[[Writable]] 表示能否修改屬性的值。前述例子這個特性默認為true
[[Value]] 包含這個屬性的數據值。讀取屬性值得實惠,從這個位置讀;寫入屬性值得實惠,把新值保存在這個位置。這個特性的默認值為undefined
要修改屬性默認的特性,必須使用ECMAScript5的Object.defineProperty()方法。
主要接收三個參數:屬性所在的對象,屬性的名字和一個描述符對象。描述符(descriptor)對象的屬性必須是:configurable,enumerable,writalbe和value。
設置其中的一或多個值,可以修改對應的特性值
var person = {}; // 創建了一個name屬性,它的值"Nicholas"是只讀的。 Object.defineProperty(person, "name", { writable: false, value: "Nicholas" }); console.log(person.name); // "Nicholas" // 在非嚴格模式下,賦值操作會被忽略 // 而在嚴格模式下,會導致錯誤 person.name = "Greg"; console.log(person.name); // "Nicholas"
把configurable設置為false,表示不能從對象中刪除屬性。如果對這個屬性調用delete,則在非嚴格模式下什么也不會發生,而在嚴格模式下會導致錯誤
一旦把屬性定義為不可配置的,就不能再把它變回可配置了。此時再調用Object.defineProperty()方法修改除writable之外的特性,都會導致錯誤
var person = {}; Object.defineProperty(person, "name", { configurable: false, value: "Nicholas" }); // 拋出錯誤 Object.defineProperty(person, "name", { configurable: true, value: "Nicholas" })
可以多次調用Object.defineProperty()方法修改同一個屬性,但在把configurable特性設置為false之后就會有限制了。
調用Object.defineProperty()如果不指定,configurable,enumerable,writalbe 特性的默認值都是false。多數情況下,沒有必要利用Object.defineProperty()方法提供的高級功能。
訪問器屬性訪問器屬性不包含數據值,它們包含一對getter和setter函數(不是必須的)
在讀取訪問器屬性時,會調用getter函數,這個函數負責返回有效的值
寫入訪問器屬性時,會調用setter函數并傳入新值,這個函數負責決定如何處理數據
訪問器屬性有如下4個特性:
[[Configurable]] 表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為數據屬性。對于直接在對象上定義的屬性,這個特性的默認值為true
[[Enumerable]] 表示能否通過for-in循環返回屬性。對于直接在對象上定義的屬性,這個特性的默認值為true
[[Get]] 在讀取屬性時調用的函數。默認值為undefined
[[Set]] 在寫入屬性時調用的函數。默認值為undefined
訪問器屬性不能直接定義,必須使用Object.defineProperty()來定義
var book = { _year: 2004, edition: 1 }; Object.defineProperty(book, "year", { get: function () { return this._year; }, set: function (newValue) { if (newValue > 2004) { this._year = newValue; this.edition += newValue - 2004; } } }); // 這是使用訪問器屬性的常見方式,即設置一個屬性的值會導致其他屬性發生變化。 book.year = 2005; console.log(book.edition); // 2
不一定非要同時制定getter和setter。只指定getter意味著屬性時不能寫,嘗試寫入屬性會被忽略。在嚴格模式下嘗試寫入只指定了getter函數的屬性會拋出錯誤。
只指定setter意味著屬性時也不能讀,否則在非嚴格模式下回返回undefined,而在嚴格模式下會拋出錯誤
支持ECMAScript 5這個方法的瀏覽器有IE9+(IE8只是部分實現),Firefox 4+, Safari 5+, Opera 12+, Chrome
在不支持Object.defineProperty()方法的瀏覽器中不能修改[[Configurable]] 和 [[Enumerable]]
定義多個屬性Object.defineProperties() 定義多個屬性的方法
var book = {} Object.defineProperties(book, { _year: { writable: true, value: 2004 }, edition: { writable: true, value: 1 }, year: { get: function() { return this._year; }, set: function(newValue) { if (newValue > 2004) { this._year = newValue; this.editio += newValue - 2004; } } } })讀取屬性的特性
Ojbect.getOwnPropertyDescriptor() 方法,可以取得給定屬性的描述符,這個方法接收兩個參數:屬性所在的對象和要讀取屬性描述符的屬性名稱。返回值時一個對象,如果是訪問器屬性,這個對象的屬性有configurable, enumerable, get, set;如果是數據屬性,這個對象的屬性有configurable, enumerable, writable, value
var book = {}; Object.defineProperties(book, { _year: { writable: true, value: 2004 }, edition: { writable: true, value: 1 }, year: { get: function() { return this._year; }, set: function(newValue) { if (newValue > 2004) { this._year = newValue; this.editio += newValue - 2004; } } } }); var descriptor = Object.getOwnPropertyDescriptor(book, "_year"); console.log(descriptor.value); // 2004 console.log(descriptor.configurable); // false console.log(typeof descriptor.get); // undefined var descriptor = Object.getOwnPropertyDescriptor(book, "year"); console.log(descriptor.value); // undefined console.log(descriptor.enumerable); // false console.log(typeof descriptor.get); // "function"創建對象
雖然Object構造函數或對象字面量都可以用來創建單個對象,但這些方式有個明顯的缺點:使用同一個接口創建很多對象,會產生大量的重復代碼。為解決這個問題,人們開始使用工廠模式的一種變體
工廠模式考慮到ECMAScript中無法創建類,開發者就發明了一種函數,用函數來封裝以特定接口創建對象的細節
function createPerson(name, age, job) { var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function () { alert(this.name); }; return o; } var person1 = createPerson("Nicholas", 29, "Software Engineer"); var person2 = createPerson("Greg", 27, "Doctor");
雖然工廠模式解決了創建多個相似對象的問題,但卻沒有解決對象識別的問題(即怎樣知道一個對象的類型)。
構造函數模式可創建自定義的構造函數,從而定義自定義對象類型的屬性和方法
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = function () { alert(this.name); }; } var person1 = new Person("Nicholas", 29, "Software Engineer"); var person2 = new Person("Greg", 27, "Doctor");
構造函數模式對比工廠模式存在以下不同之處
沒有顯式的創造對象
直接將屬性和方法賦給了this對象
沒有return 語句
函數名首字母大寫,按照慣例,構造函數都應該以一個大寫字母開頭,而非構造函數小寫
person1 和 person2 分別保存著Person的一個不同的實例。兩者都有一個constructor(構造函數)屬性,改屬性指向Person
console.log(person1.constructor == Person); // true console.log(person2.constructor == Person); // true
對象的constructor屬性最初是用來標識對象類型的。但是,檢測對象類型,還是instanceof操作符要更可靠。
這個例子中創建的所有對象既是Object的實例,同時也是Person的實例
console.log(person1 instanceof Object); // true console.log(person2 instanceof Object); // true console.log(person1 instanceof Person); // true console.log(person2 instanceof Person); // true
創建自定義的構造函數意味著將來可以將它的實例標識為一種特定的類型,這正是構造韓式模式勝過工廠模式的地方。這個例子中,person1, person2 之所以同時是Object的實例,是因為所有對象均繼承自Object
以這種方式定義的構造是定義在Global對象(瀏覽器中是window)中的。第8章將詳細討論瀏覽器對象模型(BOM)
將構造函數當作函數構造函數與其他函數的唯一區別就在于調用方式不同。
構造函數也是函數,不存在定義構造函數的特殊語法
任何函數只要通過new操作符來調用,那它就可以作為構造函數
而任何函數不通過new操作符來調用,那它跟普通函數沒有區別
// 作為構造函數使用 var person = new Person("Nicholas", 29, "Software Engineer"); person.sayName(); // "Nicholas" // 作為普通函數調用,此時this指向window對象 Person("Greg", 27, "Doctor"); // 添加到window對象上 window.sayName(); // "Greg" // 在另一個對象的作用域中調用 var o = new Object(); Person.call(o, "Kristen", 25, "Nurse"); o.sayName(); // "Kristen"構造函數的問題
構造函數模式的并非沒有缺點。主要問題就是每個方法都要在每個實例上重新創建一遍。在前述例子中person1和person2都有一個名為sayName()的方法,但那兩個方法不是同一個Function的實例。不要忘了ECMAScript中函數是對象,因此每定義一個函數,也就是實例化了一個對象。從邏輯角度講,此時的構造函數也可以這樣定義
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; // 與聲明函數再路基上是等價的 this.sayName = new Function("alert(this.name)"); } console.log(person1.sayName == person2.sayName); // false
有this對象在,沒有必要在執行代碼前就把函數綁定到特定對象上面,通過把函數定義轉移到函數外部來簡化
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = sayName } function sayName () { alert(this.name); } var person1 = new Person("Nicholas", 29, "Software Engineer"); var person2 = new Person("Greg", 27, "Doctor");
我們把sayName函數的定義轉移到了函數外部。而在構造函數內部,我們將sayName屬性設置成等于全局的sayName函數。如此一來,由于sayName包含的是一個指向函數的指針,因此person1和person2對象就共享了全局作用域中定義的同一個sayName() 函數。
新的問題出現了,在全局作用域中定義的函數實際上只能被某個對象調用,這讓全局作用域有點名不副實。而更讓人無法接受的是:如果對象需要定義很多方法,那么就要定義很多個全局函數,于是我們這個自定義的引用類型就絲毫沒有封裝性可言了。
好在可以通過原型模式來解決
原型模式我們創建的每個函數都有一個prototype(原型),這個屬性是一個指針,指向一個對象,而這個對象的用途是包含可以由特定類型的所有實例共享的屬性和方法。
prototype就是通過調用構造函數而創建的那個對象實例的原型對象,使用原型對象的好處是可以讓所有對象實例共享它所包含的屬性和方法。換言之,不必再構造函數中定義對象實例的信息,而是可以將這些信息直接添加到原型對象中
function Person() {} Person.prototype.name = "Nicholas"; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function () { alert(this.name); }; var person1 = new Person(); person1.sayName(); // "Nicholas" var person2 = new Person(); person2.sayName(); // "Nicholas" console.log(person1.sayName == person2.sayName); // true
在此,我們將sayName()方法和所有屬性直接添加到了Person的prototype屬性中,構造函數變成了空函數。即使如此,也仍然可以通過調用構造函數來創建新對象,而且新對象還會具有相同的屬性和方法。
但與構造函數模式不同的是,新對象的這些屬性和方法時由所有實例共享的。換言之,person1和person2訪問的都是同一組屬性和方法
要理解原型模式的工作原理,必須先理解ECMAScript中原型對象的性質
理解原型對象只要創建了一個函數,就會根據一組特定的規則為該函數創建一個prototype屬性,這個屬性指向函數的原型對象。
默認情況下,所有原型對象都會自動獲得一個constructor(構造函數)屬性,這個屬性是一個指向prototype屬性所在函數的指針。例如上述例子,Person.prototype.constructor 指向Person。
我們可以繼續為原型對象添加其他屬性和方法
創建了自定義構造函數后,其原型對象默認只會得到constructor屬性,至于其他方法,則都是從Object繼承而來。當調用構造函數創建一個新的實例后,該實例的內部將包含一個指針(內部屬性),指向構造函數的原型對象。ECMAScript 5 中管這個叫[[Prototype]]。
雖然腳本中沒有標準的方式訪問[[Prototype]],但Firefox, Safari, Chrome 在每個對象都支持一個屬性__proto__;這個屬性對腳本則是完全不可見的。
這個鏈接存在于實例與構造函數的原型對象之間,而不是存在于實例與構造函數之間
圖6-1(148頁)展示了Person構造函數、Person的原型屬性以及Person現有的兩個實例之間的關系。在此,Person.prototype指向了原型對象,而Person.prototype.constructor又指回了Person。原型對象中除了包含constructor屬性之外,還包括后來添加的其他屬性。Person的每個實例——person1和person2都包含一個內部屬性,該屬性僅僅指向了Person.prototype;換言之,person1和person2 與構造函數沒有直接的關系。此外,要格外注意的是,雖然這兩個實例都不包含屬性和方法,但我們卻可以調用person1.sayName()。這是通過查找對象屬性的過程來實現的。
雖然在所有實現中都無法訪問到[[Prototype]],但可以通過isPrototypeOf()方法來確定對象之間是否存在這種關系。
console.log(Person.prototype.isPrototypeOf(person1)); // true console.log(Person.prototype.isPrototypeOf(person2)); // true console.log(Person.isPrototypeOf(person1)); // false console.log(Person.isPrototypeOf(person2)); // false
ECMAScript 5 新增了一個方法,Object.getPrototypeOf(), 在所有支持的實現中,這個方法返回[[Prototype]]的值。支持的瀏覽器 IE9+, Safari 5+, Opera 12+, Chrome
console.log(Object.getPrototypeOf(person1) == Person.prototype); // true console.log(Object.getPrototypeOf(person1).name); // "Nicholas"
每當代碼讀取某個對象的某個屬性時,都會執行一次搜索,目標是具有給定名字的屬性。搜索從對象實例本身開始,如果沒有找到,則繼續搜索指針指向的的原型對象,在原型對象中查找具有給定名字的屬性。也就是說,在我們調用person1.sayName()的時候,會先執行兩次搜索,在person1中沒有找到sayName屬性,繼續在person1的原型中搜索,在Person.prototype中找到了sayName屬性,然后讀取那個保存在原型對象中的函數。
原型最初只包含constructor屬性,而該屬性也是共享的,因此可以通過對象實例訪問
雖然可以通過對象實例訪問保存在原型中的值,但卻不能通過對象實例重寫原型中的值。如果我們在實例中添加了一個屬性,而該屬性與實例原型中的一個屬性同名,那我們就在實例中創建該屬性,該屬性將會屏蔽原型中的那個屬性
hasOwnProperty()方法(繼承于Object)可以檢測一個屬性是存在于實例中,還是存在于原型中。
function Person() {} Person.prototype.name = "Nicholas"; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function () { alert(this.name); }; var person1 = new Person(); var person2 = new Person(); console.log(person1.hasOwnProperty("name")); // false person1.name = "Greg"; console.log(person1.name); // "Greg" ——來自實例 console.log(person1.hasOwnProperty("name")); // true console.log(person2.name); // "Nicholas" ——來自原型 console.log(person2.hasOwnProperty("name")); // false // 使用delete操作符刪除實例中的屬性 delete person1.name; console.log(person1.name); // "Nicholas" ——來自實例 console.log(person1.hasOwnProperty("name")); // false
ECMAScript 5 的 Object.getOwnPropertyDescriptor() 方法只能用于實例屬性,要取得原型屬性的描述符,必須直接在原型對象上調用Object.getOwnPropertyDescriptor() 方法
原型與in操作符兩種方式使用in操作符,多帶帶使用和在for-in循環中使用。
多帶帶使用in操作符,會在通過對象能夠訪問給定屬性時返回true,無論該屬性存在于實例中還是原型中。
function Person() {} Person.prototype.name = "Nicholas"; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function () { alert(this.name); }; var person1 = new Person(); var person2 = new Person(); console.log(person1.hasOwnProperty("name")); // false console.log("name" in person1); // true person1.name = "Greg"; console.log("name" in person1); // true console.log(person2.name); // "Nicholas" ——來自原型 console.log("name" in person2); // true // 使用delete操作符刪除實例中的屬性 delete person1.name; console.log("name" in person1); // true
同時使用hasOwnProperty() 方法和in操作符,就可以確定該屬性存在于實例中還是存在運行中
function hasPrototypeProperty(object, name) { return !object.hasOwnProperty(name) && (name in object); }
由于in操作符只要通過能夠訪問到屬性就返回true, hasOwnProperty() 只在屬性存在于實例中才返回true,因此只要in操作符返回true,而hasOwnproperty()返回false,就可以確定屬性是原型中的屬性。
function Person() {} Person.prototype.name = "Nicholas"; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function () { alert(this.name); }; var person = new Person(); console.log(hasPrototypeProperty(person, "name")); // true person.name = "Greg"; console.log(hasPrototypeProperty(person, "name")); // false
使用for-in循環,返回的是所有能夠通過對象訪問的、可枚舉的(enumerad)屬性,其中既包括存在于實例中的屬性,也包括存在于原型中的屬性。屏蔽了原型中不可枚舉的屬性(即將[[Enumerable]]標記為false的屬性)實例屬性也會在for-in循環返回,因為根據規定,所有開發人員定的屬性,都是可枚舉的——只有在IE8及更早版本中例外。
IE早期版本的實現中存在一個bug,即屏蔽不可枚舉屬性的實例屬性不會出現在for-in循環中。
var o = { toString: function() { return "My Object"; } }; for (var prop in o) { if (prop == "toString") { console.log("Found toString"); // 在IE中不會顯示 } }
在IE中,由于其實現認為原型的toString()方法被打上了值為false的[[Enumerable]]標記,因此應該跳過該屬性,結果就不會打印。該bug會影響默認不可枚舉的所有屬性和方法,包括:hasOwnProperty(), propertyIsEnumerable(), toLocaleString(), valueOf()
ECMAScript 5 也將constructor和prototype屬性的[Enumerable]]特性設置為false,但并不是所有瀏覽器都照此實現。
要取得對象上所有可枚舉的實例屬性,可以使用ECMAScript5的Object.keys()方法。這個方法接收一個對象作為參數,返回一個包含所有可枚舉屬性的字符串數組
function Person() {} Person.prototype.name = "Nicholas"; Person.prototype.age = 29; Person.prototype.job = "SoftWare Engineer"; Person.prototype.sayName = function() { console.log(this.name); }; var keys = object.keys(Person.prototype); console.log(keys); // "name,age,job,sayName" var p1 = new Person(); p1.name = "Rob"; p1.age = 31; var p1kyes = Object.keys(p1); console.log(p1kyes); // "name,age"
如果你想要得到所有實例屬性,無論它是否可枚舉,都可以使用Object.getOwnPropertyNames()方法。
var keys = Object.getOwnPropertyNames(Person.prototype); // "constructor,name,age,job,sayName"
注意結果中包含了不可枚舉的constructor屬性。Object.keys() 和 Object.getOwnPropertyNames()方法都可以用來替代for-in循環。支持這兩個方法的瀏覽器有IE9+, Firefox4+, Safari5+, Opera12+, Chrome
更簡單的原型語法前面例子中每添加一個屬性和方法就要敲一遍 Person.prototype。 為減少不必要的輸入,也為了從視覺上更好的封裝原型的功能,更常見的做法是用一個包含所有屬性和方法的對象字面量來重寫整個原型對象
function Person() {} Person.prototype = { name: "Nicholas", age: 29, job: "Software Engineer", sayName: function () { console.log(this.name); } };
我們將Person.prototype設置為等于一個以對象字面量形式創建的新對象。最終結果相同,但有一個例外:constructor屬性不再指向Person了。因為每創建一個函數,就會同時創建它的prototype對象,整個對象也會自動獲得constructor屬性。而我們在這里使用的語法,本質上完全重寫了默認的prototype對象,因此constructor屬性也就變成了新對象的constructor屬性(指向Object構造函數),不再指向Person函數。此時盡管instanceof操作符還能返回正確的結果,但通過constructor已經無法確定對象的類型
var friend = new Person(); console.log(friend instanceof Object); // true console.log(friend instanceof Person); // true console.log(friend.constructor == Person); // false console.log(friend.constructor == Object); // Object
如果constructor的值真的很重要,可以像下面這樣特意將它設置回適當的值。
function Person() {} Person.prototype = { constructor: Person, // 讓 prototype的constructor重新指向Person name: "Nicholas", age: 29, job: "Software Engineer", sayName: function () { console.log(this.name); } };
這種方式重設constructor會導致它的[[Enumerable]]特性被設置為true。默認情況下,原生的constructor屬性是不可枚舉的,因此如果你使用兼容ECMAScript 5 的 JavaScript 引擎,可以試一試 Object.defineProperty()
function Person() {} Person.prototype = { name: "Nicholas", age: 29, job: "Software Engineer", sayName: function () { console.log(this.name); } }; // 重設構造函數,只適用于ECMASCript 5 兼容的瀏覽器 Object.defineProperty(Person.prototype, "constructor", { enumerable: false, value: Person });原型的動態性
由于在原型中查找值的過程是一次搜索,因此我們對原型對象所做的任何修改都能夠立即從實例上反映出來——即使是先創建了實例后修改原型也照樣如此
var friend = new Person(); Person.prototype.sayHi = function() { console.log("Hi"); }; friend.sayHi(); // "Hi"
盡管可以隨時為原型添加屬性和方法,并且修改能夠立即在所有對象實例中反映出來,但如果是重寫整個原型對象,那么情況就不一樣了。調用構造函數時會為實例添加一個指向最初原型的[[Protoype]]指針,而吧原型修改為另外一個對象,就等于切斷了構造函數與最初原型之間的聯系。請記住:實例中的指針僅指向原型,而不是指向構造函數。
function Person() {} var friend = new Person(); // 重寫整個原型對象,就等于切斷了構造函數與最初原型之間的聯系 Person.prototype = { constructor: Person, age: 29, job: "Software Engineer", sayName: function () { console.log(this.name); } }; friend.sayName(); // error原生對象的原型
原型模式的重要性不僅體現在創建自定義類型方面,就連所有原生的引用類型,都是采用這種模式創建的。所有原生引用類型(Object, Array, String, 等)都在其構造函數的原型上定義了方法。例如,在Array.prototype 中可以找到sort()方法,而在String.prototype中可以找到substring()方法修改同一個屬性
console.log(typeof Array.prototype.sort); // "function" console.log(typeof String.prototype.substring); // "function"
通過原生對象的原型,不僅可以取得所有默認方法的引用,而且也可以定義新方法。可以像修改自定義對象的原型一樣修改原生對象的原型,因此可以隨時添加方法。下面的代碼就給基本包裝類型String添加了一個名為startsWith()的方法
String.prototype.startsWith = function (text) { return this.indexOf(text) == 0; }; var msg = "Hello world!"; console.log(msg.startsWith("Hello")); // true
盡管看起來很方便,但不推薦在產品化的程序修改原生對象的原型。如果某個實現中缺少某個方法,就在原生對象的原型中添加這個方法,那么當另一個支持該方法的實現中運行代碼時,就可能會導致命名沖突。而且這樣做也可能會意外的重寫原生方法。
原型對象的問題
原型模式也不是沒有缺點。
首先,它省略了為構造函數傳遞初始化參數這一環節,結果所有實例在默認情況下都將取得相同的屬性值。雖然這回在某種程度上帶來一些不方便,但還不是原型的最大問題
原型模式最大的問題是由其共享的本性鎖導致的。原型中所有屬性是被很多實例共享的,這種共享對于函數非常適合。對于那些包含基本值的屬性倒也說得過去,畢竟通過在實例上添加一個同名屬性,可以隱藏原型中的對應屬性。
然而,對于包含引用類型值得屬性來說,問題就比較突出了
function Person() {} Person.prototype = { constructor: Person, // 讓 prototype的constructor重新指向Person name: "Nicholas", age: 29, job: "Software Engineer", friend: ["Shelby", "Court"], sayName: function () { console.log(this.name); } }; var person1 = new Person(); var person2 = new Person(); // 這里修改的實際上是Person.prototype.friends person1.friends.push("Van"); // 不但person1的friends屬性被修改,person2也做了同樣的改動 console.log(person1.friends); // "Shelby,Court,Van" console.log(person1.friends); // "Shelby,Court,Van" // 因為兩個實例的friends屬性指向的都是Person.prototype.friends console.log(person1.friends === person2.friends); // true
上述問題正是很少有人多帶帶使用原型模式的原因的所在
組合使用構造函數模式和原型模式創建自定義類型的最常見方式,就是組合使用構造函數模式與原型模式。構造函數模式用于定義實例屬性,而原型模式用于定義方法和共享的屬性。這樣每個實例都會有自己的一份實例屬性的副本,但又同時共享著對方法的引用,最大限度的節省了內存。
另外這種混成模式還支持向構造函數傳遞參數
這種構造函數與原型混成的模式,是目前在ECMAScript 中使用最廣泛,認同度最高的一種創建自定義類型的方法。可以說是用來定義引用類型的一種默認模式。
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.friends = ["Shelby", "Court"]; } Person.prototype = { constructor: Person, sayName: function() { console.log(this.name); } }; var person1 = new Person("Nicholas", 29, "Software Engineer"); var person2 = new Person("Greg", 27, "Doctor"); person1.friends.push("Van"); console.log(person1.friends); // "Shelby,Court,Van" console.log(person1.friends); // "Shelby,Court" console.log(person1.friends === person2.friends); // false console.log(person1.sayName === person2.sayName); // true動態原型模式
動態原型模型把所有信息都封裝在了構造函數中,而通過在構造函數中初始化原型(僅在必要的情況下),又保持了同時使用構造函數和原型的優點。換言之,可以通過檢查某個應該存在的方法是否有效,來決定是否需要初始化原型
function Person(name, age, job) { // 屬性 this.name = name; this.age = age; this.job = job; // 方法 if (typeof this.sayName != "function") { Person.perototype.sayName = function() { console.log(this.name); }; } } var friend = new Person("Nicholas", 29, "Software Engineer"); friend.sayName(); // "Nicholas"
使用動態原型模型時,不能使用對象字面量重寫原型。如果在已經創建了實例的情況下重寫原型,那么就會切斷現有實例與新原型之間的聯系。
寄生構造函數模式在前述幾種模式都不適用的情況下,可以使用寄生(parasitic)構造函數模式。創建一個函數,該函數的作用僅僅是封裝創建的對象代碼,然后再返回新創建的對象;但從表面上看,這個函數又很像是典型的構造函數
function Person(name, age, job) { var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function() { console.log(this.name); }; return o } var friend = new Person("Nicholas", 29, "Software Engineer"); friend.sayName(); // "Nicholas"
除了使用new操作符并把使用的包裝函數叫做構造函數之外,這個模式跟工廠模式其實是一模一樣的。構造函數再不返回值得情況下,默認會返回新對象實例。而通過一個return語句,可以重寫調用構造函數時返回的值。
這個模式可以在特殊情況下用來為對象創建構造函數。假設我們想創建一個具有額外方法的特殊數組。由于不能直接修改Array構造函數,因此可以使用這個模式。
function SpecialArray() { // 創建數組 var values = new Array(); // 添加值 values.push.apply(values, arguments); // 添加方法 values.toPipedString = function() { return this.join("|"); }; // 返回數組 return values; } var colors = new SpecialArray("red", "blue", "green"); console.log(colors.toPipedString()); // "red|blue|green"
關于寄生構造函數模式,有一點需要說明:首先,返回的對象與構造函數或者與構造函數的原型屬性之間沒有關系;也就是說,構造函數返回的對象與構造函數外部創建的對象沒有什么不同。為此不能依賴instanceof操作符來確定對象類型。由于上述問題,我們建議在可以使用其他模式的情況下,不要使用寄生模式
穩妥構造函數模式
道格拉斯·克羅克福德(Douglas Crockford)發明了JavaScript中的穩妥對象(durable objects)這個概念。穩妥對象,指的是沒有公共屬性而且其方法不引用this的對象。穩妥對象最適合在一些安全的環境中(這些環境中會禁止使用this和new),或者在防止數據被其他應用程序(如Mashup程序)改動時使用。穩妥構造函數遵循與寄生構造函數類似的模式,但有兩點不同:
一是創建對象的實例方法不引用this
二是不適用new操作符調用構造函數
function Person(name, age, job) { // 創建要返回的對象 var o = new Object(); // 這里定義私有變量和函數 ... // 添加方法 o.sayName = function() { console.log(name); }; // 返回對象 return o; } var friend = Person("Nicholas", 29, "Software Engineer"); friend.sayName(); // "Nicholas"
在這種模式創建的對象中,除了使用sayName()方法之外,沒有其他辦法訪問name的值。
與寄生構造函數模式類似,使用穩妥構造函數模式創建的對象與構造函數之間也沒有什么關系,因此instanceof操作符對這種對象也沒有意義
繼承
許多OO語言都支持兩種繼承方式
接口繼承,只繼承方法簽名
實現繼承,繼承實際方法
如前所述,在ECMAScript中無法實現接口繼承,只支持實現繼承,而且其實現繼承主要是依靠原型鏈來實現的。
原型鏈
原型鏈 實現繼承的主要方法。基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。簡單回顧一下構造函數、原型和實例的關系:
每個構造函數都有一個原型對象
原型對象都包含一個指向構造函數的指針
而實例都包含一個指向原型對象的內部指針。
假如我們讓原型對象等于另一個類型的實例,顯然,此時的原型對象將包含一個指向另一個原型的指針,相應地,另一個原型中也包含著指向另一個構造函數的指針。假如另一個原型又是另一個類型的實例,那么上述關系依然成立,如此層層遞進,就構成了實例與原型的鏈條。這就是所謂原型鏈的基本概念
實現原型鏈有一種基本模式
function SuperType() { this.property = true; } SuperType.prototype.getSuperValue = function() { return this.property; }; function SubType() { this.subproperty = false; } // 繼承了SuperType SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function () { return this.subproperty; }; var instance = new SubType(); console.log(instance.getSuperValue()); //true
上述代碼中,我們沒有使用SubType默認使用的原型,而是給它換了個新原型,SuperType的實例。于是新原型不僅具有作為一個SuperType的實例所擁有的全部屬性和方法,而且其內部還有一個指針,指向了SuperType的原型。
最終:
instance指向SubType的原型
SubType的原型又指向SuperType的原型
getSuperValue() 方法仍然還在SuperType.prototype中,但property則位于SubType.prototype中。這是因為property是一個實例屬性,而getSuperValue()則是一個原型方法。 既然 SubType.prototype 現在是SuperType的實例,那么property當然就位于該實例中了。
此外,要注意instance.constructor 現在指向的是SuperType,這是因為原來SubType.prototype 中的 constructor被重寫了的緣故
通過實現原型鏈,本質上拓展了原型搜索機制。當讀取模式訪問一個實例屬性時,首先會在實例中搜索該屬性。如果沒有找到該屬性,則會繼續搜索實例的原型。在通過原型鏈實現繼承的情況下,搜索過程就得以沿著原型鏈繼續向上。調用instance.getSuperValue()會經歷三個搜索步驟
搜索實例
搜索SubType.prototype
搜索SuperType.prototype 最終找到方法
別忘記默認的原型事實上,前述例子的原型鏈少了一環。所有引用類型默認都繼承了Object,而這個繼承也是通過原型鏈實現的。所有函數的默認原型都是Object的實例,因為默認原型都會包含一個內部指針,指向Object.prototype。這也正是所有自定義類型都會繼承toString(), valueOf()等默認方法的根本原因。
SubType繼承了SuperType,而 SuperType繼承了Object。當調用instance.toString()方法,實際上調用的是保存在Object.prototype中的那個方法
確定原型和實例的關系第一種方式,使用instanceof操作符,只要用這個操作符來測試實例與原型鏈中出現過的構造函數,結果就會返回true。
// 由于原型鏈,我們可以是instance是Object,SuperType, SubType中任何一個類型的實例 console.log(instance instanceof Object); // true console.log(instance instanceof SuperType); // true console.log(instance instanceof SubType); // true
第二種方式,使用isPrototypeOf()方法。同樣,只要是原型鏈中出現過的原型,都可以說是該原型鏈所派生的實例的原型。
console.log(Object.prototype.isPrototypeOf(instance)); // true console.log(SuperType.prototype.isPrototypeOf(instance)); // true console.log(SubType.prototype.isPrototypeOf(instance)); // true謹慎的定義方法
子類型有時候需要覆蓋超類型中的一個方法,或者需要添加超類型中不存在的某個方法。但不管怎么樣,給原型鏈添加方法的代碼一定要放在替換原型的語句之后
function SuperType() { this.property = true; } SuperType.prototype.getSuperValue = function() { return this.property; }; function SubType() { this.subproperty = false; } // 繼承了SuperType,原來默認的原型被替換 SubType.protoype = new SuperType(); // 添加新方法 SubType.prototype.getSubValue = function () { return this.subproperty; }; // 重寫超類型中的方法 SubType.protoype.getSuperValue = function () { return false; }; var instance = new SubType(); console.log(instance.getSuperValue()); // false
還有一點需要提醒讀者,即在通過原型鏈實現繼承時,不能使用對象字面量創建原型方法。因為這樣就會重寫原型鏈
function SuperType() { this.property = true; } SuperType.prototype. getSuperValue = function() { return this.property; }; function SubType() { this.subproperty = false; } // 繼承了SuperType SubType.protype = new SuperType(); // 使用字面量添加新方法,會導致上一行代碼無效 // 原型鏈被切斷——SubType 和 SuperType 之間已經沒有關系 SubType.prototype = { getSubValue: function () { return this.subproperty; }, someOtherMethod: function () { return false; } }; var instance = new SubType(); console.log(instance.getSuperValue())); // error原型鏈的問題
最主要的問題,來自包含引用類型值的原型。包含引用類型值的原型屬性,會被所有實例共享;所以要在構造函數中定義值而不是原型對象。在通過原型來實現繼承時,原型實際上會變成另一個類型的實例。于是,原先的 實例屬性也就順理成章的變成了 現在的原型屬性了。
function SuperType() { this.colors = ["red", "blue", "green"]; } function SubType() { } // SubType 繼承了 SuperType 之后 // SubType.prototype 就變成了 SuperType的一個實例 // 因此 SubType.prototype 也擁有了自己的colors屬性 等價于創建了一個SubType.prototype.colors SubType.protype = new SuperType(); var instance1 = new SubType(); instance1.colors.push("black"); console.log(instance1.colors); // "red,blue,green,black" // 結果就是所有SubType實例都會共享這個colors屬性 var instance2 = new SubType(); console.log(instance2.colors); // "red,blue,green,black"
原型鏈的第二個問題:在創建子類型的實力時,不能向超類型的構造函數中傳遞參數。準確的說是沒有辦法在不影響所有對象實例的情況下,給超類型的構造函數傳遞參數。
有鑒于此,實踐中很少會多帶帶使用原型鏈
借用構造函數在解決原型中包含引用類型值所帶來的問題過程中,開發人員開始使用一種叫做 借用構造函數(constructor stealing) 的技術(有時候也叫偽造對象或經典繼承)。
思想相當簡單,即在子類型構造函數的內部調用超類型構造函數。函數只不過是在特定環境中執行代碼的對象,因此通過使用apply() call()方法也可以在(將來)新創建對象上執行構造函數。實際上是在(未來將要)新創建的SubType實例環境下,調用了SuperType構造函數,就會在新 SubType 對象上執行 SuperType() 函數中定義的所有對象初始化代碼。結果每個SubType的實例都會具有自己的colors屬性的副本了。
function SuperType() { this.colors = ["red", "blue", "green"]; } function SubType() { // 繼承了SuperType // "借調“了超類型的構造函數 SuperType.call(this); } var instance1 = new SubType(); instance1.colors.push("black"); console.log(instance1.colors); // "red,blue,green,black" // SubType實例都不會共享這個colors屬性 var instance2 = new SubType(); console.log(instance2.colors); // "red,blue,green"傳遞參數
相對于原型鏈而言,借用構造函數有一個很大的優勢,即可以在子類型構造函數中向超類型構造函數傳遞參數。
為了確保SuperType 構造函數不會重寫子類型的屬性,可以在調用超類型構造函數后,再添加應該在子類型中定義的屬性。
function SuperType(name) { this.name = name; } function SubType() { // 繼承了SuperType 同時傳遞了參數 SuperType.call(this. "Nicholas"); // 實例屬性 this.age = 29; } var instance = new SubType(); console.log(instance.anme); // "Nicholas" console.log(instance.age); // 29借用構造函數的問題
僅僅是借用構造函數,也將無法避免構造函數模式存在的問題——方法都在構造函數中定義,因此函數復用就無從談起了。
在超類型的原型中定義方法,對子類型而言也是不可見的,結果所有類型都只能使用構造函數模式。
有鑒于此,借用構造函數也是很少多帶帶使用的
組合繼承組合繼承(combination inheritance),有時候也叫作偽經典繼承,是將原型鏈和借用構造函數的技術組合到一起,從而發揮二者之長。其背后的思路是使用原型鏈實現對原型屬性和方法的繼承,而通過借用構造函數來實現對實例屬性的繼承。既通過在原型上定義方法實現了函數復用,又能保證每個實例都有它自己的屬性
function SuperType(name) { this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function() { console.log(this.name); } function SubType(name, age) { // 繼承屬性 SuperType.call(this, name); // 子類型自己的屬性 this.age = age } // 繼承方法 SubType.prototype = new SuperType(); // 如果不指定constructor,SubType.prototype.constructor 為 SuperType SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function() { console.log(this.age); }; var instance1 = new SubType("Nicholas", 29); instance1.colors.push("black"); console.log(instance1.colors); // "red,blue,green,black" instance1.sayName(); // "Nicholas" instance1.sayAge(); // 29 var instance2 = new SubType("Greg", 27); console.log(instance2.colors); // "red,blue,green" instance2.sayName(); // "Greg" instance2.sayAge(); // 27
組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優點,成為JavaScript中最常用的繼承模式。而且instanceof 和 isPrototypeOf() 也能夠用于識別基于組合繼承創建的對象。
原型式繼承道格拉斯·克羅克福德2006年在文章中介紹了一種實現繼承的方法,這種方法并沒有使用嚴格意義上的構造函數。他的想法是借助原型可以基于已有對象創建新對象,同時還不必因此創建自定義類型。為達到這個目的,給出了如下函數
function object(o) { function F() {} F.prototype = o; return new F(); }
在object() 函數內部,先創建了一個臨時性的構造函數,然后將傳入的對象作為這個構造函數的原型,最后返回了這個臨時類型的新實例。從本質上講,object() 對傳入其中的對象執行了一次淺復制
var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"], }; var anotherPerson = object(person); anotherPerson.name = "Greg"; anotherPerson.friends.push("Rob"); console.log(anotherPerson.friends); // "Shelby,Court,Van,Rob" var yetAnotherPerson = object(person); yetAnotherPerson.name = "Linda"; yetAnotherPerson.friends.push("Barbie"); console.log(yetAnotherPerson.friends); // "Shelby,Court,Van,Rob,Barbie" console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie"
這種原型式繼承,要求你必須有一個對象可以作為另一個對象的基礎。把它傳遞給object()函數,然后再根據具體需求對得到的對象加以修改即可。這意味著,person.friends不僅屬于person,而且也會被anotherPerson, yetAnotherPerson共享。實際上就相當于又創建了person對象的兩個副本(淺拷貝)。
ECMAScript 5 通過新增Object.create() 方法規范化了原型式繼承。這個方法接受兩個參數:一個用作新對象原型的對象和(可選)一個新對象定義額外的屬性的對象。在傳入一個參數的情況下,Object.create() 與 Object()方法的行為相同(原著如此表述,但實際兩者并不相同,參照第五章 Object類型 的相關補充說明)
var person = { name: "Nicholoas", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = Object.create(person); anotherPerson.name = "Greg"; anotherPerson.friends.push("Rob"); console.log(anotherPerson.friends); // "Shelby,Court,Van,Rob" var yetAnotherPerson = Object.object(person); yetAnotherPerson.name = "Linda"; yetAnotherPerson.friends.push("Barbie"); console.log(yetAnotherPerson.friends); // "Shelby,Court,Van,Rob,Barbie" console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie"
Object.create() 方法的第二個參數與Object.defineProperties() 方法的第二個參數格式相同:每個屬性都是通過自己的描述符定義的。以這種方式制定的任何屬性都會覆蓋對象上同名的屬性。
var person = { name: "Nicholoas", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = Object.create(person, { name: { value: "Greg" } }); console.log(anotherPerson.name); // "Greg"
支持Object.create()方法的瀏覽器:IE9+, Firefox4+, Opera12+, Chrome
在沒有必要興師動眾的創建構造函數,而只想讓一個對象與另一個對象保持類似的情況下,原型式繼承是完全可以勝任的。但別忘了,包含引用類型值(如上面的friends屬性是一個數組)始終都會共享相應的值,就像使用原型模式一樣。
寄生式繼承寄生式繼承(parasitic)是與原型式繼承緊密相關的一種思路,并且同樣也是由克羅克福德推而廣之的。
思路與構造函數和工廠模式類似,既創建了一個僅用于封裝繼承過程的函數,該函數在內部以某種方式來增強對象,最后再像真的是它做了所有工作一樣返回對象。
function createAnother(original) { var clone = object(original); // 通過調用函數創建一個新對象 clone.sayHi = function() { // 以某種方式增強這個對象 console.log("hi"); }; return clone; // 返回這個對象 }寄生組合式繼承
組合繼承是最常用的繼承模式;不過最大的問題就是無論什么情況下,都會調用兩次超類型構造函數:
一次是在創建子類型原型的時候
另一次是在子類型構造函數內部
也就是說,子類型最終會包含超類型對象的全部實例屬性,但我們不得不在調用子類型構造函數時,重寫這些屬性。
function SuperType(name) { this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function() { console.log(this.name); }; function SubType(name, age) { SuperType.call(this, name); // 第二次調用SuperType() this.age = age; } // 實例化SuperType作為SubType的原型 // 立即觸發 SubType.prototype = new SuperType(); // 第一次調用SuperType() SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function() { console.log(this.age); }; // 此時觸發第二次調用 var instance = new SubType("Nicholas", 29); console.log(instance.name); // "Nicholas" console.log(SubType.prototype.name); // undefined
在第一次調用SuperType構造函數時,SubType.prototype會得到兩個屬性:name和colors;它們都是SuperType的實例屬性,只不過現在位于SubType的原型中。當調用SubType構造函數時,又會調用一次 SuperType的構造函數,這一次又在新對象上創建了實例屬性name和colors屬性。于是這兩個屬性就屏蔽了原型中的兩個同名屬性。(圖6-6)
有兩組name和colors屬性,一組在實例instance上,一組在SubType的原型中。這就是調用兩次SuperType構造函數的結果
所謂寄生組合式繼承,即通過借用構造函數來繼承屬性,通過原型鏈的混成形式來繼承方法。其背后的思路是:不必為了指定子類型的原型而調用超類型的構造函數,我們所需要 的無非就是超類型原型的一個副本。本質上,就是使用寄生式繼承來繼承超類型的原型,然后再將結果指定給子類型的原型
function inheritPrototype(subType, superType) { // 創建對象 - 超類型的對象原型的副本 // 這里沒有使用new操作符,所以沒有生成SuperType的實例 // 這里沒有調用SuperType構造函數 var prototype = Object(superType.prototype) console.log(prototype == superType.prototype) // 增強對象 - 彌補因重寫原型而失去的默認constructor屬性 // 而這也將導致supert.prototype.constructor指向了subType prototype.constructor = subType; // 指定對象 - 將新創建的對象(即副本)賦值給子類型的原型。 subType.prototype = prototype; } function SuperType(name) { this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function() { console.log(this.name); }; function SubType(name, age) { SuperType.call(this, name); this.age = age; } inheritPrototype(SubType, SuperType); SubType.prototype.sayAge = function() { console.log(this.age); };
inheritPrototype() 函數實現了寄生組合式繼承的最簡單形式。這個函數接受兩個參數: 子類型構造函數和超類型構造函數。
這樣就只調用了一次SuperType構造函數,并且因此便了SubType.prototype 上創建不必要的、多余的屬性。與此同時,原型鏈還能保持不變;因此,還能夠正常使用instanceof 和 isPrototypeOf().
開發人員普遍認為寄生組合式繼承是引用類型最理想的繼承范式。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/109479.html
摘要:表示應該立即下載腳本,但不應妨礙頁面中的其他操作可選。表示通過屬性指定的代碼的字符集。表示腳本可以延遲到文檔完全被解析和顯示之后再執行。實際上,服務器在傳送文件時使用的類型通常是,但在中設置這個值卻可能導致腳本被忽略。 第1章 JavaScript 簡介 雖然JavaScript和ECMAScript通常被人們用來表達相同的含義,但JavaScript的含義比ECMA-262要多得多...
摘要:具體說就是執行流進入下列任何一個語句時,作用域鏈就會得到加長語句的塊。如果局部環境中存在著同名的標識符,就不會使用位于父環境中的標識符訪問局部變量要比訪問全局變量更快,因為不用向上搜索作用域鏈。 基本類型和引用類型的值 ECMAscript變量包含 基本類型值和引用類型值 基本類型值值的是基本數據類型:Undefined, Null, Boolean, Number, String ...
摘要:定義函數表達式的方式有兩種函數聲明。不過,這并不是匿名函數唯一的用途。可以使用命名函數表達式來達成相同的結果閉包匿名函數和閉包是兩個概念,容易混淆。匿名函數的執行環境具有全局性,因此其對象通常指向通過改變函數的執行環境的情況除外。 定義函數表達式的方式有兩種: 函數聲明。它的重要特征就是 函數聲明提升(function declaration hoisting) 即在執行代碼之前會...
摘要:引用類型的值對象是引用類型的一個實例。引用類型是一種數據結構,用于將數據和功能組織在一起。對數組中的每一項運行給定函數,如果該函數對任一項返回,則返回。組零始終代表整個表達式。所以,使用非捕獲組較使用捕獲組更節省內存。 引用類型的值(對象)是引用類型的一個實例。 引用類型是一種數據結構,用于將數據和功能組織在一起。它同行被稱為類,但這種稱呼并不妥當,盡管ECMAScript從技術上講...
摘要:技術的核心是對象即。收到響應后,響應的數據會自動填充對象的屬性,相關的屬性有作為響應主體被返回的文本。收到響應后,一般來說,會先判斷是否為,這是此次請求成功的標志。中的版本會將設置為,而中原生的則會將規范化為。會在取得時報告的值為。 Ajax(Asynchronous Javascript + XML)技術的核心是XMLHttpRequest對象,即: XHR。雖然名字中包含XML,但...
摘要:用戶代理檢測用戶代理檢測是爭議最大的客戶端檢測技術。第二個要檢測是。由于實際的版本號可能會包含數字小數點和字母,所以捕獲組中使用了表示非空格的特殊字符。版本號不在后面,而是在后面。除了知道設備,最好還能知道的版本號。 檢測Web客戶端的手段很多,各有利弊,但不到萬不得已就不要使用客戶端檢測。只要能找到更通用的方法,就應該優先采用更通用的方法。一言蔽之,先設計最通用的方案,然后再使用特定...
閱讀 2851·2023-04-26 01:02
閱讀 1877·2021-11-17 09:38
閱讀 805·2021-09-22 15:54
閱讀 2910·2021-09-22 15:29
閱讀 897·2021-09-22 10:02
閱讀 3449·2019-08-30 15:54
閱讀 2014·2019-08-30 15:44
閱讀 1605·2019-08-26 13:46