一起掃蕩JavaScript(九) —— this重定向與模組間方法互

coyan發表於2021-09-09

this重定向,第一反應就是透過call,apply,那為什麼要改變this?改變this在實際場景中又有怎樣的應用?call,apply方法這麼cool,又能否自己實現一個?
先來看一個場景。

假設有一個陣列[1,23,123,45,6,7,8231,21,89],求出陣列中最大的數。

你很自然的就會想到用一個for迴圈遍歷陣列,並不斷的比對各個值,找到最大者:

 const maxArrVal = arr => {
     
     let res = arr[0];

     for(let i = 1; i < arr.length; i ++) {

         res = Math.max(res, arr[i]);
     }
     return res;
 }
 console.log(maxArrVal([1,23,123,45,6,7,8231,21,89]));

你會發現這裡實際上執行的僅僅是max函式,只不過需要執行的物件的資料型別是一個陣列,而Array原型中並沒有這樣的一個max函式,你會說,唉,假如能這樣就好了。

Array.max(arr)

當然,這是不現實的對吧?不可能一個已有的邏輯在另一個模組還得重複的被實現一遍,假如程式這樣設計,你就會發現,整個語言的內建物件越來越龐大臃腫,而這顯然不是設計的初衷想要的。但是呢,在實際的應用中,又有這樣的剛需,那得怎麼辦呢?
看看下面的程式碼:

 const maxArrValByApply = arr => Math.max.apply(null, arr);
 
 console.log(maxArrValByApply([1,23,123,45,6,7,8231,21,89]));

這是call,apply在實際應用中的一個常見的場景,模組間方法的互調。
實際上,這裡把不同資料型別之間的內建物件來這裡比喻成模組似乎不是特別的恰當,因為不同的資料型別處理方法很難有相同,但這有一定的相通之道。
call,apply另一個很常見的應用是this重定向,因為this在JavaScript中是一個很cool的東西,有時候也讓人頭疼,而有一個專門約束的機制是再好不過的了。
再看下面這個小例子。
toString是幾乎每一種的資料型別(除了null,undefined)都會有的一個方法,它的作用顧名思義就是轉成字串。比如:

 let object = {name: 'dorsey'},
     number = 12312313,
     array = [123,214],
     string = 'dasdas',
     boolean = true;
     
 console.log(object.toString()); //[object Object]
 console.log(number.toString()); //12312313
 console.log(array.toString());//123,214
 console.log(string.toString());//dasdas
 console.log(boolean.toString());//true
 console.log(Symbol().toString());//Symbol()

物件比較特殊,它內建的轉化成字串的時候轉成的結果是[object Object],在JavaScript中,萬物皆物件,String,Number,Boolean,Array其實都是一個物件,或者說都是一個建構函式,你會發現typeof Array這樣的實際上是一個function,所以它們都可以用new關鍵字,既然都是物件,所以物件中的toString方法在這裡實際上是可以用的,這經常被用來做安全的型別校驗,如下:

console.log(Object.prototype.toString.apply([1,3])); //  [object Array]

而這裡的toString執行的主體實際上變成了[1,3],而這就是this的重定向。這樣可以很方便的做安全的型別檢查。
那apply,call方法實際上是怎麼實現的呢?

很自然的就可以想到,將方法當做一個屬性暫時性的放到執行的這個物件環境(作用域)裡,執行後將該屬性刪除。

 Function.prototype.dorseyApply = function (context) {

     context.fn = this;
     let res = context.fn();
     delete context.fn;
     return res;
 }

這樣會有一個問題,傳入的這個作用域,可能本身就包含了屬性fn,這樣會把原來的fn給重置,顯然不是我們想要的,這時候很顯然就想到了Symbol,變成這樣:

Function.prototype.dorseyApply = function (context) {

    let _fn = Symbol();
    context[_fn] = this;
    let res = context[_fn]();
    delete context[_fn];
    return res;
}

但apply中是可以傳入一個陣列作為引數的,而這裡並沒有引數。而且,但沒傳入this時,預設是window物件,怎麼做呢?看一下:

 Function.prototype.dorseyApply = function (context) {
     context = context || window;
     let _fn = Symbol();
     context[_fn] = this;
     let res = typeof arguments[1] === 'undefined' ? context[_fn]() : context[_fn](...arguments[1]);;
     delete context[_fn];
     return res;
 }

但這樣還有問題,因為引用型別的沒有問題,但假如說是值型別的呢?比如這樣呼叫:

console.log(Object.prototype.toString.dorseyApply(Symbol()));

先看一個簡單的例子:

 let a = 'dorsey';
 a.hello = '你好啊';
 console.log(a.hello);   // undefined

你會發現是undefined,為什麼呢?因為值型別的資料不能透過屬性訪問的方式來操作,掛載屬性等等。那怎麼做呢?但看似矛盾的你會發現一些方法比如split,toString是掛載在上面的,實際上是放在了原型中,作用域我們知道,既然在a中找不到,那就會在原型中找。而我們要做的就是往原型(prototype)中新增一個屬性。

Function.prototype.dorseyApply = function (context) {

    context = context || window;

    let _fn = Symbol(), res;

    let _type = typeof context;

    let _map = {

        'object': context,
        'string': String.prototype,
        'number': Number.prototype,
        'boolean': Boolean.prototype,
        'symbol': Symbol.prototype
    }

    _map[_type][_fn] = this;
    
    res = typeof arguments[1] === 'undefined' ? _map[_type][_fn]() : _map[_type][_fn](...arguments[1]);

    delete _map[_type][_fn];

    return res;
}

這樣是不是就好了呢?我們試一下:

let name = 'window作用域的name1';    //  注意用let宣告的name在window裡是訪問不到的
let obj0 = {
   name: 'dorsey',
   fn (age) {
       console.log("我的名字叫:" + this.name, "我" + age + "歲");
   }
};
let obj1 = {
   name: 'sen'
};
console.log("=====dorseyApply=========");
obj0.fn();	
obj0.fn.dorseyApply();
obj0.fn.dorseyApply(null, [25]);
obj0.fn.dorseyApply(obj1, [25]);
Math.max.dorseyApply(null,[1,23,123,45,6,7,831,21,89]));
Object.prototype.toString.dorseyApply(Symbol());

console.log("======apply========");
obj0.fn();
obj0.fn.apply();
obj0.fn.apply(null, [25]);
obj0.fn.apply(obj1, [25]);
Math.max.apply(null,[1,23,123,45,6,7,831,21,89]));
Object.prototype.toString.apply(Symbol());

看起來好像沒什麼問題。因為apply第2個引數是需要傳一個陣列。但也有特例,比如傳入一個類物件陣列也是可以的。看看下面的:

Array.dorseyApply(null, {length: 10})

你會發現這時候你就坑了,其實這個只需要轉成真陣列就好了。

Function.prototype.dorseyApply = function (context) {

    context = context || window;

    let _fn = Symbol(), res;

    let _type = typeof context;

    let _map = {

        'object': context,
        'string': String.prototype,
        'number': Number.prototype,
        'boolean': Boolean.prototype,
        'symbol': Symbol.prototype
    }

    _map[_type][_fn] = this;
    
    res = typeof arguments[1] === 'undefined' ? _map[_type][_fn]() : _map[_type][_fn](...Array.from(arguments[1]));

    delete _map[_type][_fn];

    return res;
}

這樣就OK了。其實這樣不是特別好,因為在這裡為了簡便用了Array.from這樣的方法,而在一個通用的方法中,最好是不要有這樣某個模組下的方法的。
那call函式怎麼實現呢?其實這兩個幾乎是一樣,只不過call的傳參方式是

call(this, args1, args2 ...);

而apply是

apply(this, [args1, args2 ...]);

這樣其實只需要把apply的實現稍微轉化一下就好了。

Function.prototype.dorseyCall = function (context) {

    context = context || window;
    
    let _fn = Symbol(), res;

    let _type = typeof context,
        
        _map = {

        'object': context,
        'string': String.prototype,
        'number': Number.prototype,
        'boolean': Boolean.prototype,
        'symbol': Symbol.prototype
    }

    _map[_type][_fn] = this;

    res = typeof arguments[1] === 'undefined' ? _map[_type][_fn]() : _map[_type][_fn](...[...arguments].slice(1, arguments.length));

    delete _map[_type][_fn];

    return res;
}

好了,關於this重定向的call,apply這兩個就暫時先介紹到這。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2236/viewspace-2822351/,如需轉載,請註明出處,否則將追究法律責任。

相關文章