美文网首页
[underscore 源码学习] 整体结构 & 面向对象风格支

[underscore 源码学习] 整体结构 & 面向对象风格支

作者: 小黄人get徐先生 | 来源:发表于2020-01-27 18:02 被阅读0次

1. Underscore 结构

作用域包裹

与其他第三方库一样,Underscore 也通过 立即执行函数 来包裹自己的业务逻辑。

目的:

  • 避免全局污染:所有库的逻辑,库所定义和使用的变量全部被封装到了该函数的作用域中。
  • 隐私保护:但凡在立即执行函数中声明的函数、变量等,除非是自己想暴露,否则绝无可能在外部获得。
// function 外面加括号,使之成为表达式
(function() {
  // ... 执行逻辑
})()

2. 面向对象风格支持

_对象

underscore 有下划线的意思,所以 underscore 通过一个下划线变量 _ 来标识自身。

注意:
_是一个函数对象,之后所有的 api 都会被挂载到这个对象上,如 _.each_.map

var _ = function (obj) {
  if (obj instance _) {
    return obj;
  }
  if (!(this instanceof _)) {
    return new _(obj);
  }
  this._wrapped = obj;
}

_()

虽然 Underscore 推崇函数式编程,但也支持面向对象风格的函数调用,仅需要通过_()来包裹对象即可。

当我们进行如下调用时:
_([2, 3, 4])
会创建一个新的 underscore 对象(从而能够调用 underscore 提供的方法),并在 this._wrapped 中存储传入的数据。


3. mixin

// 源码
_.mixin = function(obj) {
  _.each(_.function(obj), function(name) {
    var func = _[name] = obj[name];
    _.prototype[name] = function() {
      var args = [this._wrapped];
      push.apply(args, arguments);
      return chain(this, func.apply(_, args));
    };
  });
  return _;
};
  • mixin(混入)模式是增加代码复杂度的一种广泛使用的设计模式。
  • _.mixin(obj):为 underscore 对象混入 obj 具有的功能。

4. 链接式调用

  • jQuery 链接式调用
    $('.div').css('color', 'red').show();
  • 想要实现链式调用,通常我们会在支持链式调用的函数中返回本身。
var RULES = function() {
  add: function(x) { console.log(x); return this; },
  mult: function(y) { console.log(y); return this; }
};
RULES.add(4).mult(5);
  • 但是,这样做并不优雅,这需要我们手动地在函数中添加 return this 语句。更好的做法是我们需要创建一个通用函数,它能为我们指定的对象添加链式调用机制。

_.chain()

  • _.chain(obj): 为 underscore 对象的方法添加链式调用能力。
  • _.chain 源码如下:
_.chain = function(obj) {
  var instance = _(obj);
  instance._chain = true;
  return instance;
};
  • underscore 还提供了一个帮助函数 result,该函数将会判断方法调用结果,如果该方法的调用者被标识了需要链化,则链化当前的方法执行结果。
var chainResult = function(instance, obj) {
  return instance._chain ? _(obj).chain() : obj;
};

下面开始我们的源码实现:

主要有两个文件 test.jsindex.html

  1. 首先实现一个基本的框架和 unique 功能
    test.js
(function(root) {

    const _ = function() {};

    _.unique = function(obj, callback) {
        const res = [];
        for (let i = 0; i < obj.length; i++) {
            const val = callback? callback(obj[i]) : obj[i];
            if (res.indexOf(val) === -1) {
                res.push(val);
            }
        }
        return res;
    };

    root._ = _; // 下面的 this 指向 window,所以这里是将 _ 变量挂到 window 对象下。
})(this);

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>underscore</title>
</head>
<body>
    <script src="./test.js"></script>
    <script>
        console.log(_.unique([1,2,3,4,1,2,'a','A'],(val) => {
            return typeof val === 'string'? val.toLowerCase() : val;
        }));
    </script>
</body>
</html>

打开 console 发现输出为 [1, 2, 3, 4, "a"],unique 功能成功实现。

问题:如果我们想以面向风格的形式调用,该怎么实现呐?

_([1,2,3,4,1,2,'a','A']).unique((val) => {
            return typeof val === 'string'? val.toLowerCase() : val;
        }))

因此,我们需要在变量 _ 构造函数的原型上扩展相应的方法。为了避免方法的重写,我们使用 _.mixin 方法帮我们实现静态方法的原型方法实现。

  1. 使用 _.mixin 方法实现静态方法的原型方法实现
    test.js
(function(root) {

    const toString = Object.prototype.toString;
    const push = Array.prototype.push;

    const _ = function(obj) {
        // 如果 obj 是 _ 实例对象,则直接返回
        if (obj instanceof _) {
            return obj;
        }
        // 如果 instanceof 不是 _ 实例对象(this 一开始指向 window),则调用 new _(obj);当再次调用的时候(此时 this 指向 _ 实例对象),则下面方法不执行,
        if (!(this instanceof _)) {
            return new _(obj);
        }
        // 将值存储到实例对象的 _wrapped 属性当中。
        this._wrapped = obj;
    };

    _.unique = function(obj, callback) {
        const res = [];
        for (let i = 0; i < obj.length; i++) {
            const val = callback? callback(obj[i]) : obj[i];
            if (res.indexOf(val) === -1) {
                res.push(val);
            }
        }
        return res;
    };

    // 判断对象是否为数组对象
    _.isArray = function(obj) {
        return toString.call(obj) === "[object Array]";
    };

    // 返回对象的属性列表 
    _.functions = function(obj) {
        const res = [];
        for (let key in obj) {
            res.push(key);
        }
        return res;
    };
    // 循环遍历值并执行回调
    _.each = function(obj, callback) {
        if (_.isArray(obj)) {
            for (let i = 0;i < obj.length; i++) {
                callback.call(obj, obj[i], i);
            }
        } else {
            for (let key in obj) {
                callback.call(obj, key, obj[key]);
            }
        }
    };

    _.mixin = function(obj) {
        _.each(_.functions(obj), (name) => {
            const func = obj[name]; // 获取到对应的静态方法(例如:_.unique 方法)

            // 扩展静态方法到对象原型上
            _.prototype[name] = function() {
                // 下面的内容是为了处理参数,具体原因可以看看 index.html 里面的调用方式。被处理的值在初始化时传入,其他参数在调用原型方法时传入。
                let args = [this._wrapped];
                push.apply(args, arguments);
                return func.apply(this, args);
            };
        });
    };

    _.mixin(_);

    root._ = _;
})(this);

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>underscore</title>
</head>
<body>
    <script src="./test.js"></script>
    <script>
        console.log(_([1,2,3,4,1,2,'a','A']).unique((val) => {
            return typeof val === 'string'? val.toLowerCase() : val;
        }));
    </script>
</body>
</html>

打开 console 发现输出为 [1, 2, 3, 4, "a"],功能成功实现。

问题:如上所示,结果直接输出的是数组值;如果我们想实现方法的链式调用呐?即,在 unique 方法后再调用 map 方法呐?

因为输出结果是个数组,不可能再调用实例对应的方法,所以我们需要在调用完 unique 方法后,返回 _ 的实例对象,并且把返回的值传给 _ 来进行初始化(_([1,2,3,4,"a"]))。

_([1,2,3,4,1,2,'a','A']).unique((val) => {
            return typeof val === 'string'? val.toLowerCase() : val;
        })).map()
  1. 使用 _.chain() 方法实现链式调用
    test.js
(function(root) {

    const toString = Object.prototype.toString;
    const push = Array.prototype.push;

    const _ = function(obj) {

        if (obj instanceof _) {
            return obj;
        }

        if (!(this instanceof _)) {
            return new _(obj);
        }
        this._wrapped = obj;
    };

    _.unique = function(obj, callback) {
        const res = [];
        for (let i = 0; i < obj.length; i++) {
            const val = callback? callback(obj[i]) : obj[i];
            if (res.indexOf(val) === -1) {
                res.push(val);
            }
        }
        return res;
    };

    _.map = function(args) {
        return args;
    };

    _.isArray = function(obj) {
        return toString.call(obj) === "[object Array]";
    };

    _.functions = function(obj) {
        const res = [];
        for (let key in obj) {
            res.push(key);
        }
        return res;
    };

    _.each = function(obj, callback) {
        if (_.isArray(obj)) {
            for (let i = 0;i < obj.length; i++) {
                callback.call(obj, obj[i], i);
            }
        } else {
            for (let key in obj) {
                callback.call(obj, key, obj[key]);
            }
        }
    };

    // 生成实例对象,并设置实例属性 _.chain 为 true
    _.chain = function(obj) {
        const instance = _(obj);
        instance._chain = true;
        return instance;
    };

    // 如果实例拥有 _chain 属性,则返回 _(obj).chain(),实现链式调用;否则直接返回值结束。
    const result = function(instance, obj) {
        // 下面的 chain 为静态方法
        return instance._chain? _(obj).chain() : obj;
    };

    _.mixin = function(obj) {
        _.each(_.functions(obj), (name) => {
            const func = obj[name];

            _.prototype[name] = function() {
                let args = [this._wrapped];
                push.apply(args, arguments);
                return result(this, func.apply(this, args));
            };
        });
    };

    _.mixin(_);

    root._ = _;
})(this);

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>underscore</title>
</head>
<body>
    <script src="./test.js"></script>
    <script>
        console.log(_([1,2,3,4,1,2,'a','A']).chain().unique((val) => {
            return typeof val === 'string'? val.toLowerCase() : val;
        }).map());
    </script>
</body>
</html>

输出结果如下:



成功实现链式调用,让我们的数据流像在管道一样传输。

问题:我们的结果最终是返回一个对象,如果我们想结束链式调用返回值呐,应该怎么实现?

  1. 添加 _.prototype.value 方法返回值
    test.js
(function(root) {

    const toString = Object.prototype.toString;
    const push = Array.prototype.push;

    const _ = function(obj) {

        if (obj instanceof _) {
            return obj;
        }

        if (!(this instanceof _)) {
            return new _(obj);
        }
        this._wrapped = obj;
    };

    _.unique = function(obj, callback) {
        const res = [];
        for (let i = 0; i < obj.length; i++) {
            const val = callback? callback(obj[i]) : obj[i];
            if (res.indexOf(val) === -1) {
                res.push(val);
            }
        }
        return res;
    };

    _.map = function(args) {
        return args;
    };

    _.isArray = function(obj) {
        return toString.call(obj) === "[object Array]";
    };

    _.functions = function(obj) {
        const res = [];
        for (let key in obj) {
            res.push(key);
        }
        return res;
    };

    _.each = function(obj, callback) {
        if (_.isArray(obj)) {
            for (let i = 0;i < obj.length; i++) {
                callback.call(obj, obj[i], i);
            }
        } else {
            for (let key in obj) {
                callback.call(obj, key, obj[key]);
            }
        }
    };

    _.chain = function(obj) {
        const instance = _(obj);
        instance._chain = true;
        return instance;
    };

    const result = function(instance, obj) {
        return instance._chain? _(obj).chain() : obj;
    };

    _.prototype.value = function() {
        return this._wrapped;
    };

    _.mixin = function(obj) {
        _.each(_.functions(obj), (name) => {
            const func = obj[name];

            _.prototype[name] = function() {
                let args = [this._wrapped];
                push.apply(args, arguments);
                return result(this, func.apply(this, args));
            };
        });
    };

    _.mixin(_);

    root._ = _;
})(this);

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>underscore</title>
</head>
<body>
    <script src="./test.js"></script>
    <script>
        console.log(_([1,2,3,4,1,2,'a','A']).chain().unique((val) => {
            return typeof val === 'string'? val.toLowerCase() : val;
        }).map().value());
    </script>
</body>
</html>

打开 console 发现输出为 [1, 2, 3, 4, "a"],功能成功实现。

相关文章

网友评论

      本文标题:[underscore 源码学习] 整体结构 & 面向对象风格支

      本文链接:https://www.haomeiwen.com/subject/rccethtx.html