underscore 系列之內部函式 cb 和 optimizeCb

冴羽發表於2017-11-29

前言

僅看 cb 和 optimizeCb 兩個函式的名字,你可能想不到這是用來做什麼的,儘管你可能想到 cb 是 callback 的縮寫。

如果直接講解原始碼,你可能想不明白為什麼要這麼寫,所以我們從 _.map 函式開始講起。

_.map

_.map 類似於 Array.prototype.map,但更加健壯和完善。我們看下 _.map 的原始碼:

// 簡化過,這裡僅假設 obj 是陣列
_.map = function (obj, iteratee, context) {
    iteratee = cb(iteratee, context);

    var length = obj.length, results = Array(length);
    for (var index = 0; index < length; index++) {
        results[index] = iteratee(obj[index], index, obj);
    }

    return results;
};複製程式碼

map 方法除了傳入要處理的陣列之外,還有兩個引數 iteratee 和 context,類似於 Array.prototype.map 中的其他兩個引數,其中 iteratee 表示處理函式,context 表示指定的執行上下文,即 this 的值。

然後在原始碼中,我們看到,我們將 iteratee 和 context 傳入一個 cb 函式,然後覆蓋掉 iteratee 函式,然後將這個函式用作最終的處理函式。

實際上,需要這麼麻煩嗎?不就是使用 iteratee 函式處理每次迭代的值嗎?不就是通過 context 指定 this 的值嗎?我們可以直接這樣寫吶:

_.map = function (obj, iteratee, context) {
    var length = obj.length, results = Array(length);
    for (var index = 0; index < length; index++) {
        results[index] = iteratee.call(context, obj[index], index, obj);
    }
    return results;
};

// [2, 3, 4]
console.log(_.map([1, 2, 3], function(item){
    return item + 1;
})) 

// [2, 3, 4]
console.log(_.map([1, 2, 3], function(item){
    return item + this.value;
}, {value: 1}))複製程式碼

你看看也沒有什麼問題吶,可是,萬一 iteratee 我們不傳入一個函式呢?比如我們什麼也不傳,或者傳入一個物件,又或者傳入一個字串、數字呢?

如果用我們的方法自然是會報錯的,那 underscore 呢?

// 使用 underscore

// 什麼也不傳
var result = _.map([1,2,3]); // [1, 2, 3]

// 傳入一個物件
var result = _.map([{name:'Kevin'}, {name: 'Daisy', age: 18}], {name: 'Daisy'}); // [false, true]

var result = _.map([{name: 'Kevin'}, {name: 'Daisy'}], 'name'); // ['Kevin', 'daisy']複製程式碼

我們會發現,underscore 竟然還能根據傳入的值的型別不同,實現的效果不同。我們總結下:

  1. 當 iteratee 不傳時,返回一個相同的陣列。
  2. 當 iteratee 為一個函式,正常處理。
  3. 當 iteratee 為一個物件,返回元素是否匹配指定的物件。
  4. 當 iteratee 為字串,返回元素對應的屬性值的集合。

由此,我們可以推測在 underscore 的 cb 函式中,有對 iteratee 值型別的判斷,然後根據不同的型別,返回不同的 iteratee 函式。

cb

所以我們來看看 cb 函式的原始碼:

var cb = function(value, context, argCount) {

    if (_.iteratee !== builtinIteratee) return _.iteratee(value, context);

    if (value == null) return _.identity;

    if (_.isFunction(value)) return optimizeCb(value, context, argCount);

    if (_.isObject(value) && !_.isArray(value)) return _.matcher(value);

    return _.property(value);
};複製程式碼

這一看就牽扯到了 8 個函式!不要害怕,我們一個一個看。

_.iteratee

if (_.iteratee !== builtinIteratee) return _.iteratee(value, context);複製程式碼

我們看看 _.iteratee 的原始碼:

_.iteratee = builtinIteratee = function(value, context) {
    return cb(value, context, Infinity);
};複製程式碼

因為 _.iteratee = builtinIteratee 的緣故,_.iteratee !== builtinIteratee 值為 false,所以正常情況下 _.iteratee(value, context) 並不會執行。

但是如果我們在外部修改了 .iteratee 函式,結果便會為 true,cb 函式直接返回 `.iteratee(value, context)`。

這個意思其實是說用我們自定義的 _.iteratee 函式來處理 value 和 context。

試想我們並不需要現在 _.map 這麼強大的功能,我只希望當 value 是一個函式,就用該函式處理陣列元素,如果不是函式,就直接返回當前元素,我們可以這樣修改:

<html>
<head>
    <title>underscore map</title>
</head>
<body>
    <script src="../vender/underscore.js"></script>
    <script type="text/javascript">
    _.iteratee = function(value, context) {
        if (typeof value === 'function') {
            return function(...rest) {
                return value.call(context, ...rest)
            };
        }
        return function(value) {
            return value;
        };
    };

    // 如果 map 的第二個引數不是函式,就返回該元素
    console.log(_.map([1, 2, 3], 'name')); // [1, 2, 3]

    // 如果 map 的第二個引數是函式,就使用該函式處理陣列元素
    var result = _.map([1, 2, 3], function(item) {
        return item + 1;
    });

    console.log(result); // [2, 3, 4]
    </script>
</body>
</html>複製程式碼

當然更多的情況是自定義對不同的 value 使用不同的處理函式,值得注意的是,underscore 中的多個函式都是用了 cb 函式,而因為 cb 函式使用了 _.iteratee 函式,如果你修改這個函式,其實會影響多個函式,這些函式基本都屬於集合函式,具體包括 map、find、filter、reject、every、some、max、min、sortBy、groupBy、indexBy、countBy、sortedIndex、partition、和 unique。

_.identity

if (value == null) return _.identity;複製程式碼

讓我們看看 _.identity 的原始碼:

_.identity = function(value) {
    return value;
};複製程式碼

這也就是為什麼當 map 的第二個引數什麼都不傳的時候,結果會是一個相同陣列的原因。

_.map([1,2,3]); // [1, 2, 3]複製程式碼

如果直接看這個函式,可能覺得沒有什麼用,但用在這裡,卻又十分的合適。

optimizeCb

if (_.isFunction(value)) return optimizeCb(value, context, argCount);複製程式碼

當 value 是一個函式的時候,就傳入 optimizeCb 函式,我們來看看 optimizeCb 函式:

var optimizeCb = function(func, context, argCount) {
    // 如果沒有傳入 context,就返回 func 函式
    if (context === void 0) return func;
    switch (argCount) {
        case 1:
            return function(value) {
                return func.call(context, value);
            };
        case null:
        case 3:
            return function(value, index, collection) {
                return func.call(context, value, index, collection);
            };
        case 4:
            return function(accumulator, value, index, collection) {
                return func.call(context, accumulator, value, index, collection);
            };
    }
    return function() {
        return func.apply(context, arguments);
    };
};複製程式碼

也許你會好奇,為什麼我要對 argCount 進行判斷呢?就不能直接返回嗎?比如這樣:

var optimizeCb = function(func, context) {
    // 如果沒有傳入 context,就返回 func 函式
    if (context === void 0) return func;
    return function() {
        return func.apply(context, arguments);
    };
};複製程式碼

當然沒有問題,但為什麼 underscore 要這樣做呢?其實就是為了避免使用 arguments,提高一點效能而已,如果不是寫一個庫,其實還真是沒有必要做到這點。

而為什麼當引數是 3 個時候,引數名稱分別是 value, index, collection ,又為什麼沒有引數為 2 的情況呢?其實這都是根據 underscore 函式用到的情況,沒有函式用到兩個引數,於是就省略了,像 map 函式就會用到 3 個引數,就根據這三個引數的名字起了這裡的變數名啦。

_.matcher

if (_.isObject(value) && !_.isArray(value)) return _.matcher(value);複製程式碼

這段就是用來處理當 map 的第二個引數是物件的情況:

// 傳入一個物件
var result = _.map([{name:'Kevin'}, {name: 'Daisy', age: 18}], {name: 'Daisy'}); // [false, true]複製程式碼

如果 value 是一個物件,並且不是陣列,就使用 _.matcher 函式。看看各個函式的原始碼:

var nativeIsArray = Array.isArray;

_.isArray = nativeIsArray || function(obj) {
    return Object.prototype.toString.call(obj) === '[object Array]';
};

_.isObject = function(obj) {
    var type = typeof obj;
    return type === 'function' || type === 'object' && !!obj;
};


// extend 函式可以參考 《JavaScript 專題之手寫一個 jQuery 的 extend》
_.matcher = function(attrs) {
    attrs = _.extend({}, attrs);
    return function(obj) {
      return _.isMatch(obj, attrs);
    };
};

// 該函式判斷 attr 物件中的鍵值是否在 object 中有並且相等

// var stooge = {name: 'moe', age: 32};
// _.isMatch(stooge, {age: 32}); => true

// 其中 _.keys 相當於 Object.keys
_.isMatch = function(object, attrs) {
    var keys = _.keys(attrs), length = keys.length;
    if (object == null) return !length;
    var obj = Object(object);
    for (var i = 0; i < length; i++) {
        var key = keys[i];
        if (attrs[key] !== obj[key] || !(key in obj)) return false;
    }
    return true;
};複製程式碼

_.property

return _.property(value);複製程式碼

這個就是處理當 value 是基本型別的值的時候,返回元素對應的屬性值的情況:

var result = _.map([{name: 'Kevin'}, {name: 'Daisy'}], 'name'); // ['Kevin', 'daisy']複製程式碼

我們看下原始碼:

_.property = function(path) {
    // 如果不是陣列
    if (!_.isArray(path)) {
      return shallowProperty(path);
    }
    return function(obj) {
        return deepGet(obj, path);
    };
};

var shallowProperty = function(key) {
    return function(obj) {
        return obj == null ? void 0 : obj[key];
    };
};

// 根據路徑取出深層次的值
var deepGet = function(obj, path) {
    var length = path.length;
    for (var i = 0; i < length; i++) {
        if (obj == null) return void 0;
        obj = obj[path[i]];
    }
    return length ? obj : void 0;
};複製程式碼

我們好像發現了新大陸,原來 value 還可以傳一個陣列,用來取深層次的值,舉個例子:

var person1 = {
    child: {
        nickName: 'Kevin'
    }
}

var person2 = {
    child: {
        nickName: 'Daisy'
    }
}

var result = _.map([person1, person2], ['child', 'nickName']); 
console.log(result) // ['Kevin', 'daisy']複製程式碼

最後

如果你想學習 underscore 的原始碼,在分析集合相關的函式時一定會接觸 cb 和 optimizeCb 函式,先掌握這兩個函式,會幫助你更好更快的解讀原始碼。

underscore 系列

underscore 系列目錄地址:github.com/mqyqingfeng…

underscore 系列預計寫八篇左右,重點介紹 underscore 中的程式碼架構、鏈式呼叫、內部函式、模板引擎等內容,旨在幫助大家閱讀原始碼,以及寫出自己的 undercore。

如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎 star,對作者也是一種鼓勵。

相關文章