1. 程式人生 > >JS物件與原型

JS物件與原型

## 一. JS的物件 ### 1.1 建立物件的幾種方式 #### 1.1.1 通過字面量建立物件 在js中,一對`{}` 其實就是一個物件 ```js var person = { name: "tom", age: 23, read: function () { console.log(name, ": read book") } } ``` #### 1.1.2 通過系統的建構函式 通過系統的建構函式建立一個空的物件,然後用js動態語言的特性,如果一個物件沒有某個屬性或者方法,那麼我們點一下再附上值就好了 ```js var person2 = new Object() person2.name = "jerry" person2.age = 23 person2.say = function () { console.log(person2.name, ": say hello") } ``` #### 1.1.3 通過自定義構造方法 自定義構造方法一般都是首字母大寫的函式 ```js function Person(name, age, sex) { this.name = name this.age = age this.sex = sex this.say = function () { console.log(this.name, " :say hello") } } // 建立物件時,使用 new 關鍵字 p = new Person("tom", 23, "man") console.log(p instanceof Person) ``` 自定義的構造方法建立物件,會經歷如下幾個步驟 1. 開闢空間 2. 將this設定成當前物件 3. 初始化屬性和方法 4. 將this返回 #### 1.1.4 工廠模式建立物件 ```js function Person(name,age,sex) { // new Object 作為當前的返回值 var obj = new Object() obj.name = name obj.age = age obj.sex = sex obj.say = function () { console.log(this.name," :say hello") } // 手動將物件返回出去 return obj } // 工廠模式建立物件,不需要使用new 關鍵字 var p = Person("tom",23,"man") console.log(p instanceof Person) // false ``` ### 1.2 建構函式與例項物件 看下面的例子: ```js // 建構函式和例項的關係 function Person(name) { this.name = name this.say = function () { console.log(this.name," :say hello") } } // 物件p是通過 自定義的建構函式Person創建出來的 var p = new Person("tom") console.dir(p) console.dir(Person) ``` 列印的結果如下: * 例項物件的`__proto__`屬性中有constructor屬性,上面記錄著自己的構造方法。 * Person是構造方法,也是物件,我們直接列印Person得到的結果中有個屬性prototype,它裡面也有個屬性叫做 constructor。裡面記錄著構造方法就是自己本身。 * 結合上面的例子,我們其實可以得到這樣的推斷,**例項物件的原型屬性 和 建構函式的原型屬性中的constructor都指向了同一個構造方法** ,然後可以進一步推斷 `p是Person型別`。 > **__prototype__實際上就是原型物件,在下文中會詳細的說** ![](https://img2020.cnblogs.com/blog/1496926/202005/1496926-20200506203839158-480745402.png) 還是上面的例子,看如下的輸出也就能理解了 ```js console.log(p.constructor === Person) // true console.log(p.__proto__.constructor == Person) // true console.log(p.__proto__.constructor == Person.prototype.constructor) // true // 由此推斷出,p === Person console.log(p instanceof Person) // true ``` 其實有個小問題,看上面程式碼的第一行`console.log(p.constructor === Person)` 我們通過上面的程式碼也看不到例項物件p constructor屬性啊,怎麼就能用,也不報錯undefined呢? 其實這就是牽扯到js物件的原型鏈了,(下面的章節會說),總的來說,就是js的物件會優先使用構造方法中的屬性和方法,如果建構函式中不存在我們使用的屬性和方法的話,就嘗試去這個物件所對應的構造方法中的原型物件中的屬性和方法,再沒有就會報錯。 ## 二. JS的原型 ### 2.1 引入原型的必要性 為什麼會突然再來看js的原型呢? 因為看到了vue的原始碼中,大量的方法都被新增再vm的原型上,所以,回顧一下原型肯定是躲不過去了。 一般我們使用原型就是為了節省空間。 想理解節省了什麼空間? 那就看看下面這個不節省空間的例子。 ```js // 建構函式建立物件帶來的問題 function Person(name) { this.name = name this.say = function () { console.log(this.name,": say hello") } } var p1 = new Person("tom") var p2 = new Person("jerry") p1.say() // tom : say hello p2.say() // jerry : say hello // todo 返回false, 表示說,p1和p2的say方法,並不是同一份, 其實這並不是一件好事 console.log(p1.say == p2.say) ``` 上面的p1 和 p2 都是通過一個建構函式創建出來的不同物件,他們裡面都有say這個函式,當我們輸出 `p1.say == p2.say` 時,返回了false,說明每個物件中都有一份say方法,那假設有1000個物件,豈不是就有1000個say方法了? 這肯定是浪費空間的。 那麼有沒有辦法可以讓每次new出來的物件都使用一份say方法呢? 當然,如下: ```js // 共享函式,引出原型 function Say() { console.log(this.name, ": say hellp") } function Person(name) { this.name = name this.say = Say } var p1 = new Person("tom") var p2 = new Person("jerry") p1.say()// tom : say hellp p2.say()// jerry : say hellp // 這樣的話,確實能實現節省空間,但是容易出問題 console.log(p1.say == p2.say) // ture ``` 現在確實實現了我們的需求,但是不夠優雅,而且統一出現問題,js是動態型別的語言,那我們像下面這樣,假設不知道已經有Say這個函數了,然後將`var Say = "hello"`放置在第Say函式之後,就會產生覆蓋。 ### 2.2 認識原型 看下的例子:我們往構造方法的原型物件上新增一個say方法。 其實這塊也不是不好理解,你想啊,js的物件通過構造方法創建出來,我們把公共的方法,屬性放在構造方法的原型物件中,是不是就可以讓他們共享這些方法和屬性呢? ```js function Person(name) { this.name = name } // 在原型上新增方法 // 為什麼可以說原型是物件呢? 想想js中一個物件可以通過 點 , 動態點新增屬性和方法? Person.prototype.say = function () { console.log(this.name,":say hello") } var p1 = new Person("tom") var p2 = new Person("jerry") p1.say()//tom :say hello p2.say()//jerry :say hello console.log(p1.say == p2.say) // true ``` 通過`console.dir()`列印下上面的例項物件和建構函式,得到如下圖: ```js console.dir(p1) console.dir(p2) console.dir(Person) ``` ![](https://img2020.cnblogs.com/blog/1496926/202005/1496926-20200506203902736-1202905116.png) 通過上圖可以看到,可以得到下面的結論: * 例項物件中的直接擁有的標準屬性,比如name, 這些都是直接出現在構造方法中的屬性,而且這些屬性是js物件所私有的。 * 上圖中例項物件有個屬性叫做:`__proto__` , 這個屬性是用來給瀏覽器使用的,而不是給程式設計師使用,所以我們稱它為非標準屬性。 此外谷歌瀏覽器是支援這個屬性的,但是在IE8瀏覽器中,我們執行這句`console.log(p1.__proto__)` 會報錯,說undefined ### 2.3 原型,例項物件,建構函式之間到底是什麼關係呢? 1. 例項物件是通過 new 建構函式創建出來的,所以建構函式是建立例項物件的模版。 2. 建構函式就是那個首字母大寫的函式,在js裡面我們能直接`console.log(建構函式)` 因為這個建構函式其實也是個物件。 3. 原型的作用我們說了,就是為了將公共的方法抽取出來,全部存放在建構函式的原型物件中,而實現資料的共享,節省記憶體。 4. 例項物件的·`__proto__`, 是個非標準屬性,也是個物件,這個物件指向了 構造方法的`prototype` 屬性。 5. 構造方法的 `prototype`屬性是個標準屬性, 同時也是個物件,我們對通過 `構造方法.prototype.屬性/方法 = XXX` 的方式為其新增屬性和方法。 6. 我們通過 `物件.屬性/方法`時, 會優先從物件的構造方法中查詢,如果找不到的會再嘗試從原型中查詢,這也是為什麼會出現一個明明沒有為一個物件新增相應的屬性或者方法但是物件卻能點出來,並且能正常使用。當然如果原型中也不存在的話,就會報錯說 undefined ### 2.4 關於this物件 * 看下面的第一個例子 下面出現的this並不難理解, 就是我們new 出來的物件本身 ```js function Person(name) { // 考慮一下,這個this是誰? this.name = name console.log(this) } var p = new Person("tom") ``` * 看下面的第二個例子 我們在構造方法的原型物件上新增一個方法say,在這say方法中使用的this物件指的同樣是 我們new 出來的物件本身。即方法的呼叫者。 ```js function Person(name) { // 考慮一下,這個this是誰? this.name = name console.log("n10: ",this) } Person.prototype.say = function () { // todo 這裡的this指的是誰呢? // 首先,方法是新增在原型物件上, 那麼this指的是原型物件嗎? // 通過控制檯可以看到,this.name ,其實不是原型物件,而是say()方法的呼叫者(例項物件) console.log("n16: ",this.name,": say hello") } var p1 = new Person("tom") var p2 = new Person("jerry") p1.say() p2.say() ``` * 看下面的第三個例子: 下面在給構造方法的原型物件新增方法時,不僅出現了this, 還出現了that。 this物件依然是我們手動new出來的物件本身。 that同樣是指向了我們new出來的物件本身,之所以需要中轉一下,是**因為在按鈕的點選事件裡面,this指向的是按鈕本身。** ```js // 用面向物件的方式封裝建構函式 function ChangeStyle(btnId, dvId, color) { this.btnObj = document.getElementById(btnId) this.dv = document.getElementById(dvId) this.color = color } // 在構造方法的原型上新增方法 ChangeStyle.prototype.init = function () { // 這裡面的this表示的是 呼叫init方法的例項物件 var that = this this.btnObj.onclick = function () { // todo 為什麼原型中的函式中,就不能使用this,而是that呢??? // todo 或者問下,當前函式中的this是誰呢? that.dv.style.backgroundColor = that.color } } ``` ### 2.5 其他原型的寫法 * 最常見的寫法就是像下面這樣,在當前原型的基礎上新增屬性或者方法 ```js function Person(name) { this.name = name } // 前面的例子中我們都是像下面這樣寫程式碼, 這其實是對原來的 原型物件屬性的累加 // 原來的原型物件中有個屬性,叫做consturctor Person.prototype.say = function(){ //todo } ``` * 也可以像下面這樣 這樣設定原型的話,實際上是對原來的原型物件的覆蓋,所以說需要像下面這樣重新新增constructor的指向。 當然我也試了一下,如果說覆蓋原來的原型物件,且不新增contructor的指向,我們使用 instanceof 判斷例項物件是否是對應的建構函式型別時,還是能得到正確的結果。 ```js Person.prototype = { constructor:Person, // 手動修改構造器的指向 height:"20", weight:"20", say:function () { // todo } } ``` ### 2.6 方法之間的相互訪問 * 建構函式中的成員方法是可以相互訪問的。 ```js function Person(name) { this.name = name this.say = function () { console.log("say") // 通過這個例子,可以看到,物件的方法中可以直接呼叫物件的方法 this.eat() } this.eat = function () { console.log("eat") } } ``` * 原型中的方法也是可以相互訪問的。 ```js function Person(name) { this.name = name } Person.prototype.say = function(){ console.log("say") // 原型中的方法也可以相互訪問 this.eat() } Person.prototype.eat = function(){ console.log("eat") } var p1 = new Person("tom") p1.say() ``` ### 2.7 覆蓋內建物件原型中的方法 像這樣就可以實現對原型中的方法進行覆蓋的操作。 當然可以通過在原型上新增方法實現對原有封裝類的拓展。 ```js // 在現有的js封裝類上幹這件事,也算是在修改原始碼 String.prototype.myReverse = function () { for (var i = 0; i < this.length; i++) { console.log("發生倒敘") } } var str = "123" str.myRever