JS對象與原型

一. JS的對象

1.1 創建對象的幾種方式

1.1.1 通過字面量創建對象

在js中,一對{} 其實就是一個對象

var person = {
            name: "tom",
            age: 23,
            read: function () {
                console.log(name, ": read book")
            }
        }

1.1.2 通過系統的構造函數

通過系統的構造函數創建一個空的對象,然後用js動態語言的特性,如果一個對象沒有某個屬性或者方法,那麼我們點一下再附上值就好了

  			var person2 = new Object()
        person2.name = "jerry"
        person2.age = 23
        person2.say = function () {
            console.log(person2.name, ": say hello")
        }

1.1.3 通過自定義構造方法

自定義構造方法一般都是首字母大寫的函數

			 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 工廠模式創建對象

     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 構造函數與實例對象

看下面的例子:

   			// 構造函數和實例的關係
        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__實際上就是原型對象,在下文中會詳細的說

還是上面的例子,看如下的輸出也就能理解了

 			  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的原型上,所以,回顧一下原型肯定是躲不過去了。

一般我們使用原型就是為了節省空間。

想理解節省了什麼空間? 那就看看下面這個不節省空間的例子。

  			// 構造函數創建對象帶來的問題
        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方法呢?

當然,如下:

			 // 共享函數,引出原型
        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的對象通過構造方法創建出來,我們把公共的方法,屬性放在構造方法的原型對象中,是不是就可以讓他們共享這些方法和屬性呢?

				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()列印下上面的實例對象和構造函數,得到如下圖:

   		  console.dir(p1)
        console.dir(p2)
        console.dir(Person)

通過上圖可以看到,可以得到下面的結論:

  • 實例對象中的直接擁有的標準屬性,比如name, 這些都是直接出現在構造方法中的屬性,而且這些屬性是js對象所私有的。
  • 上圖中實例對象有個屬性叫做:__proto__ , 這個屬性是用來給瀏覽器使用的,而不是給程式設計師使用,所以我們稱它為非標準屬性。 此外Google瀏覽器是支援這個屬性的,但是在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 出來的對象本身

 			  function Person(name) {
            // 考慮一下,這個this是誰?
            this.name = name
            console.log(this)
        }

        var p = new Person("tom")
  • 看下面的第二個例子

我們在構造方法的原型對象上添加一個方法say,在這say方法中使用的this對象指的同樣是 我們new 出來的對象本身。即方法的調用者。

      	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指向的是按鈕本身。

    // 用面向對象的方式封裝構造函數
    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 其他原型的寫法

  • 最常見的寫法就是像下面這樣,在當前原型的基礎上添加屬性或者方法
 		function Person(name) {
        this.name = name
    }

    // 前面的例子中我們都是像下面這樣寫程式碼, 這其實是對原來的  原型對象屬性的累加
    // 原來的原型對象中有個屬性,叫做consturctor
    Person.prototype.say  = function(){
        //todo
    }
  • 也可以像下面這樣

這樣設置原型的話,實際上是對原來的原型對象的覆蓋,所以說需要像下面這樣重新添加constructor的指向。

當然我也試了一下,如果說覆蓋原來的原型對象,且不添加contructor的指向,我們使用 instanceof 判斷實例對象是否是對應的構造函數類型時,還是能得到正確的結果。

   Person.prototype = {
        constructor:Person, // 手動修改構造器的指向
        height:"20",
        weight:"20",
        say:function () {
            // todo
        }
    }

2.6 方法之間的相互訪問

  • 構造函數中的成員方法是可以相互訪問的。
   function Person(name) {
        this.name = name
        this.say = function () {
            console.log("say")
            // 通過這個例子,可以看到,對象的方法中可以直接調用對象的方法
            this.eat()
        }
        this.eat = function () {
            console.log("eat")
        }
    }
  • 原型中的方法也是可以相互訪問的。
    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封裝類上干這件事,也算是在修改源碼
    String.prototype.myReverse = function () {
        for (var i = 0; i < this.length; i++) {
            console.log("發生倒敘")
        }
    }
    var str = "123"
    str.myReverse()
Tags: