js的this、call、apply、bind、繼承、原型鏈0.前言1.this2.call、apply、bind3.從call到繼承

0.前言

這些都是js基礎進階的必備了,有時候可能一下子想不起來是什麼,時不時就回頭看看基礎,增強硬實力。

1.this

1.1this指向

誰最後調用,就指向誰 先簡單複習一次,this指向就那麼幾種:

  1. new 關鍵字 指向new 創建的對象
function F() {
        this.name = 1
}
var f = new F()
複製代碼
  1. call、apply、bind 指向傳入函數的第一個參數。a.call(b),函數a內部如果是要用到this。則這個this指向b
  2. 對象的方法 對象內部的方法指向對象本身
 var obj = {
                value: 5,
                printThis: function () {
                    console.log(this);
                }
            };
複製代碼
  1. 按值傳遞 指向全局
 var obj = {
                value: 5,
                printThis: function () {
                    console.log(this);
                }
            };
var f = obj.printThis
f()
複製代碼

如果出現上面對條規則的累加情況,則優先級自1至4遞減,this的指向按照優先級最高的規則判斷。

5.箭頭函數 指向箭頭函數定義時外層上下文

var obj = {
                value: 5,
                printThis: function () {
                        return function(){ console.log(this)}
                   }
            };
obj.printThis()//window

 var obj = {
                value: 5,
                printThis: function () {
                                      return () => console.log(this)
                                 }
            };
obj.printThis()()//obj

複製代碼

2.call、apply、bind

前兩者都是一樣,只是參數表現形式不同,bind表示的是靜態的前兩者,需要手動調用 a.call(b,args)讓函數a執行上下文指向b,也就是b的屬性就算沒有a函數,也能像b.a(args)這樣子調用

方法大家都知道,我們不妨來自己實現一下這三個:

2.1 call實現

再看一次概念,b沒有a方法,也就是沒有b.a,如果想要這個效果,那就利用這三個函數來改變執行上下文。於是我們就可以想到,要是自己實現一個,大概就是,給b強行加上這個a 的方法,然後拿到argument去調用:

Function.prototype.mycall = function(){
	var ctx = arguments[0]||window||global//獲取上下文,call的第一個參數
	var len = arguments.length
	var hash = new Date().getTime()//避免名字重複
	ctx[hash] = this//將this緩存,this就是那個想在另一個上下文利用的函數
	var result
	if(len === 1){
		result = ctx[hash]()//如果後面沒有其他參數直接運行
	} else{
		var i = 1
		var args = []
		for(;i<len;i++){
			args.push(arguments[i])
		}
		args = args.join(',')
		result = eval('ctx[hash](' + args + ')')//將參數傳遞進去調用
	}
	delete ctx[hash]//刪除臨時增加的屬性
	return result
}
複製代碼

apply也是同理,而且少了數組這一步,更加簡單接下來我們看一下bind怎麼實現:

Function.prototype.mybind = function(){
	var ctx = arguments[0]||window||global
	var f = this
	var args1 = []
	if(arguments.length>1){//預先填入的參數
		var i = 1
		for(;i < arguments.length;i++){
			args1.push(arguments[i])
		}
	}
	return function(){
		var args2 = Array.prototype.slice.call(arguments)//call和apply我們都可以實現,這裏就不再重複
		return f.apply(ctx,args1.concat(args2))//將預先填入的參數和執行時的參數合併
	}
}
複製代碼

此外,需要注意的,一個函數被bind後,以後無論怎麼用call、apply、bind,this指向都不會變,都是第一次bind的上下文

3.從call到繼承

首先,js沒有嚴格意義上的子類父類,實現繼承是依靠原型鏈來實現類似於所謂的類的效果。

3.1 call繼承(構造函數繼承)

我們希望G繼承F,或者是說,開發的時候,由於G有很多屬性繼承F我們想偷懶,那麼就可以這樣

function F(name,age){
  this.name = name 
  this.age = age
}
function G(name,age,a) {
  F.call(this,...arguments)
  this.a = a
}
var g = new G('a',12,1) //G {name: "a", age: 12, a: 1}
複製代碼

這個方法特別之處是,子類可以向父類構造函數傳參。但是,無法獲取F的原型上的屬性。 另外,方法也是寫在內部 this.f = function(){} 也註定無法實現函數複用了,每一個實例都有一個函數,浪費內存。

3.2 prototype繼承

要想子類獲得父類的屬性,如果是通過原型來實現繼承,那麼就是父類的一個實例是子類的原型:

function F(){
  this.a = [1,2,3,4]
  this.b = 2
}
var f = new F()
function G(){}
G.prototype = f
var g = new G()
var h = new G()
g.a //[1,2,3,4]
g.b //2
//對於引用類型,如果我們修改g.a(不是用=賦值,用=不會操作到原型鏈)
g.a.push(123)
g.a//[1,2,3,4,123]
//而且其他的實例也會變化
h.a //[1,2,3,4,123]
g.b = 666 //只是在實例裏面對b屬性進行改寫,不會影響原形鏈
複製代碼

可以看見,對於父類的引用類型,某個值是引用類型的屬性被改寫後,子類的所有的實例繼承過來的屬性都會變,主要的是,子類都可以改變父類。但是=賦值操作相當於直接在某一個實例上面改寫。因爲屬性查找是按照原型鏈查找,先查找自身再查找原型鏈,找到爲止。用了等號,先給自身賦值,所以自身賦值成功了也不會繼續去原型鏈查找。

因爲都有各自的缺陷,所以就有一種組合繼承,將構造函數繼承和prototype繼承混合起來,方法寫在父類的prototype上,是比較常見的方法。但是實例化都會調用兩次構造函數,new和call

3.3Object.create繼承(原型繼承)

這樣子,可以在兩個prototype中間加上一個中介F類,使得子類不會污染父類,子類A是父類B繼承而來,而且還可以在中間給他定義屬性

function A() {}  
function B() {}  
A.prototype = Object.create(B.prototype,{father:{value:[1,2,3]}});

//Object.create的hack
Object.create =Object.create|| function (o) {
    var F = function () {};
    F.prototype = o;
    return new F();
}
//其實create函數內部的原理就是這樣子,看回去上面的A和B,這些操作相當於
var F = function () {};
F.prototype = B.prototype;//原型被重寫,a.__proto__.constructor是B而不是F
A.prototype = new F()

//create方法,第二個參數類似於defineProperty,而且定義的屬性可以自行配置,默認是不可以重新賦值
var a = new A()
a.father //[1,2,3]
a.father = 1
a.father //[1,2,3]
複製代碼

在不需要動用構造函數的時候,只是想看到讓子類父類這種繼承關係,create基本上是完美選擇

3.4 寄生式繼承

利用一個封裝好繼承過程的函數來實現繼承,不需要另外定義一個子類,直接把子類的方法寫在函數裏面

function createobj (obj) {
	var temp = Object.create(obj)
	temp.f = function () {
		console.log('this is father')
	}
	return temp
}
function B() {}  
var b = createobj (B.prototype)
b.f() //this is father
複製代碼

但是,不能做到函數複用,每一個實例都要寫一份,而且寫了一個createobj就是寫死了,也不能獲取B類的內部屬性

3.5 寄生組合式繼承

對於上面的僅僅依靠Object.create繼承,a.__proto__原型對象被重寫,他的構造函數是B,而不是中間量F,對於這種中間類F無意義,而且只是依靠中間原型對象,我們可以用比較完美的寄生組合式繼承:

function A() {}  
function B() {}  
var prototype = Object.create(B.prototype)//創建
prototype.constructor = A//增強
A.prototype = prototype//指定,這下a.__proto__.constructor 就是A了
var a = new A()
複製代碼

不用創建中間類F,而且構造函數A的確是造出a的(a.proto.constructor == A),而不是像create那樣改寫原型鏈,構造函數是B

附上原型鏈圖解:(注意終點是null,中間的都是正常new構造,沒有改寫prototype)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章