zoukankan      html  css  js  c++  java
  • Node.js 中开源库探秘 object-assign | 全栈之路

    这篇内容呢,讲的是另一个技术栈 Node.js 系列,虽然和咱们这里的主题不是特别吻合,不过嘛,汲取多样性的养分是快速成长的好方法,也是现在流行的全栈工程师的必经之路。

    由于这篇内容涉及的是 Node.js 社区相关技术,所以要更好的读懂相关代码,还需要有一些 javascript 的基础知识。

    咱们开始进入正题, Node.js 是一套服务端体系,也是非常流行和好用的框架。Node.js 社区高人辈出,比如大名鼎鼎的 TJ Holowaychuk,就是 Node 社区的大神级人物。关于他如何被称为大神的问题,大家可以参看知乎上的解答:

    http://zhuanlan.zhihu.com/FrontendMagazine/19572823

    其中有一句话很有意思:

    要问到我是“如何”学习的——没什么特别的地方,也不读书,从不去听课,我就是去阅读别人的代码,并搞清楚那些代码是如何工作的。

    确实是这样,读代码是一种最好的学习方式。特别是 Node.js 的社区资源非常丰富,数不尽的开源库。

    如何使用

    那么咱们就来探索一个叫做 object-assign 的开源库吧。这个库的地址在这里:

    https://github.com/sindresorhus/object-assign

    它的作用也非常的简单:

    var objectAssign = require('object-assign');
    
    objectAssign({foo: 0}, {bar: 1});
    //=> {foo: 0, bar: 1}
    
    // multiple sources
    objectAssign({foo: 0}, {bar: 1}, {baz: 2});
    //=> {foo: 0, bar: 1, baz: 2}
    
    // overwrites equal keys
    objectAssign({foo: 0}, {foo: 1}, {foo: 2});
    //=> {foo: 2}
    
    // ignores null and undefined sources
    objectAssign({foo: 0}, null, {bar: 1}, undefined);
    //=> {foo: 0, bar: 1}
    

    这个库只对外暴露了一个函数,它的作用就是将参数中的几个对象合并到一起。简单解释一下哈。

    var objectAssign = require('object-assign'); 这行代码的作用是将 objc-assign 库引入到当前的代码中,并用 objectAssign 符号作为引用。引入完成后,接下来就可以调用了。 比如:

    objectAssign({foo: 0}, {bar: 1});
    

    这个调用传入了两个对象 - {foo: 0}{bar: 1},函数的作用就是将他们合并成一个对象,然后返回:{foo: 0, bar: 1}

    并且,如果参数中对象的属性有重叠,会用后面对象参数中得属性覆盖前面的,比如:

    objectAssign({foo: 0}, {foo: 1}, {foo: 2});
    

    这个最终得到的结果是 {foo: 2}。 因为这次参数中的三个对象,都包含 foo 属性,最后一个将前两个覆盖了。

    好了,关于这个库的基本使用方式咱们说完了,接下来就看看它的代码吧。

    分析代码

    object-assign 只有一个源文件,而且,所有的代码都在这里了:

    'use strict';
    var propIsEnumerable = Object.prototype.propertyIsEnumerable;
    
    function ToObject(val) {
    	if (val == null) {
    		throw new TypeError('Object.assign cannot be called with null or undefined');
    	}
    
    	return Object(val);
    }
    
    function ownEnumerableKeys(obj) {
    	var keys = Object.getOwnPropertyNames(obj);
    
    	if (Object.getOwnPropertySymbols) {
    		keys = keys.concat(Object.getOwnPropertySymbols(obj));
    	}
    
    	return keys.filter(function (key) {
    		return propIsEnumerable.call(obj, key);
    	});
    }
    
    module.exports = Object.assign || function (target, source) {
    	var from;
    	var keys;
    	var to = ToObject(target);
    
    	for (var s = 1; s < arguments.length; s++) {
    		from = arguments[s];
    		keys = ownEnumerableKeys(Object(from));
    
    		for (var i = 0; i < keys.length; i++) {
    			to[keys[i]] = from[keys[i]];
    		}
    	}
    
    	return to;
    };
    
    

    首先,来看这两行:

    'use strict';
    var propIsEnumerable = Object.prototype.propertyIsEnumerable;
    

    第一行 'use strict' 是一个代码指示,表示这个文件使用 javascript 严格语法标准。紧接着的这行,是将 Object.prototype.propertyIsEnumerable 方法做一个引用,以备后面使用。

    提到这里顺便说一句,javascript 中的函数和变量是都可以赋值给另一个变量的,并且如果变量指向的是一个函数,也可以通过这个变量来调用它指向的函数。这点 Swift 与它比较相似。

    然后我们跳过中间部分,来看最下面的部分:

    module.exports = Object.assign || function (target, source) {
    	var from;
    	var keys;
    	var to = ToObject(target);
    
    	for (var s = 1; s < arguments.length; s++) {
    		from = arguments[s];
    		keys = ownEnumerableKeys(Object(from));
    
    		for (var i = 0; i < keys.length; i++) {
    			to[keys[i]] = from[keys[i]];
    		}
    	}
    
    	return to;
    };
    

    module.exports 是 Node.js 的一个通用变量,它表示当前模块对外导出的函数,也就是在外面调用这个模块时,是 module.exports 中的内容。

    赋值操作是通过一个逻辑判断来进行的,首先判断了 Object.assign 方法是否存在,如果已经存在就直接用这个方法了。

    这个主要是基于 JS 引擎的版本考虑的,老版本的 ECMAScript 6 以下引擎是不支持 Object.assign 函数的,所以我们就必须自己实现,这个判断的作用就是这样。

    接下来,如果 Object.assign 判断失败了,我们就要使用我们自己对 assigin 操作的实现了:

    function (target, source) {
    	var from;
    	var keys;
    	var to = ToObject(target);
    
    	for (var s = 1; s < arguments.length; s++) {
    		from = arguments[s];
    		keys = ownEnumerableKeys(Object(from));
    
    		for (var i = 0; i < keys.length; i++) {
    			to[keys[i]] = from[keys[i]];
    		}
    	}
    
    	return to;
    };
    

    这个实现中,对 target 变量调用了 ToObject(target) 方法,实际上是对 target 做了一次非空判断,我们来看看 ToObject 的实现细节:

    function ToObject(val) {
    	if (val == null) {
    		throw new TypeError('Object.assign cannot be called with null or undefined');
    	}
    
    	return Object(val);
    }
    

    确实如此,ToObjectval 进行了一个判断,如果它的值为 null 就抛出异常,如果正常,就返回这个对象。

    好了 ToObject 分析完了,我们再回头看 assign 函数。调用完成后,我们将结果存放到了 to 变量中:

    var to = ToObject(target);
    

    接下来,通过一个循环,将后面几个参数的对象中的属性与 to 对象进行合并:

    for (var s = 1; s < arguments.length; s++) {
      from = arguments[s];
      keys = ownEnumerableKeys(Object(from));
    
      for (var i = 0; i < keys.length; i++) {
        to[keys[i]] = from[keys[i]];
      }
    }
    
    

    注意这个 - var s = 1; 我们之所以将 s 其实值设置为 1,是因为我们要跳过第一个参数,因为第一个参数就是我们的目标, to 变量的值。

    然后将每个参数取出来后,临时存放到 from 变量中,然后调用 ownEnumerableKeys 函数获取 from 变量中可遍历的属性,我们再来看看 ownEnumerableKeys 函数的定义:

    function ownEnumerableKeys(obj) {
    	var keys = Object.getOwnPropertyNames(obj);
    
    	if (Object.getOwnPropertySymbols) {
    		keys = keys.concat(Object.getOwnPropertySymbols(obj));
    	}
    
    	return keys.filter(function (key) {
    		return propIsEnumerable.call(obj, key);
    	});
    }
    
    

    先调用了 getOwnPropertyNames 获取了这个对象所有的属性名。

    接下来,判断了 Object.getOwnPropertySymbols 方法是否存在。这个方法是干什么的呢,这时 ES 6 标准新引进的一个特性,为了防止命名冲突。InfoQ 的这篇文章中有非常详细的介绍 http://www.infoq.com/cn/articles/es6-in-depth-symbols?utm_campaign=infoq_content&utm_source=infoq&utm_medium=feed&utm_term=global

    简单来说,如果我们当前所使用的 javascript 解析引擎是支持 ES 6 的话,那么用 var keys = Object.getOwnPropertyNames(obj); 方法并不能得到所有的属性键值,还需要进行一下这个操作:

    if (Object.getOwnPropertySymbols) {
      keys = keys.concat(Object.getOwnPropertySymbols(obj));
    }
    

    这样我们才能得到所有的键值。这个其实是新的 javascript 标准中的一个特性,就是 javascript 对象,现在除了属性名,每个属性名还对应了不同的 Symbol 之后获取了这个 Symbol 后才能得到真正的属性名称。

    着了对 Symbol 做了简单的介绍,更加详细的描述,大家可以参考 InfoQ 的那篇文章。

    最后,调用了这个方法:

    return keys.filter(function (key) {
      return propIsEnumerable.call(obj, key);
    });
    

    filter 方法会根据条件筛选数组中的元素,生成一个新的元素。筛选条件就是调用的我们最初看到的 propIsEnumerable 变量中的方法。再次判断了一下属性的有效性。

    现在,这个属性集合准备好了。 我们再次回到最初调用的地方:

    for (var s = 1; s < arguments.length; s++) {
      from = arguments[s];
      keys = ownEnumerableKeys(Object(from));
    
      for (var i = 0; i < keys.length; i++) {
        to[keys[i]] = from[keys[i]];
      }
    }
    

    这个 for 循环用 ownEnumerableKeys 方法的到要遍历的后面几个参数中的有效属性的名称,存入 keys 变量中。

    然后紧接着,遍历这个 keys 集合,将 from 对象中得属性赋值给 to 对象相应的属性,如果有同名的属性,就会用 from 中得值覆盖 to 的原始值。

    这样,object-assign 的所有代码就都分析完了。

    结论回顾

    object-assign 的代码量非常少,但是经过咱们这样分析一下,是不是感觉麻雀虽小,五脏俱全呢。这里面包含了很多 javascript 的特性,以及大部分教程类书籍都不常提及的细节处理。比如:

    为什么要使用 module.exports = Object.assign ||function(){ ... } 这样的写法。它用来判断 JS 引擎的兼容性。像是这种代码,恐怕在大多教科书类的内容中会很少强调,但在实际应用中,为了加强代码的健壮性,却是非常重要。这也是读代码学习编程的最大好处,让我们可以从实际生产环境的角度去思考问题。

    关于这个开源库的分析就到这里啦,还是那句话,水平有限,只为抛砖引玉给大家提出一个思路,相信各位的聪明才智一定能够发现更多。

  • 相关阅读:
    Java实现 蓝桥杯VIP 算法训练 一元三次方程
    Java实现 蓝桥杯VIP 算法训练 乘法表
    Java实现 蓝桥杯VIP 算法训练 矩阵加法
    Java实现 蓝桥杯VIP 算法训练 一元三次方程
    Java实现 蓝桥杯VIP 算法训练 平方计算
    Java实现 蓝桥杯VIP 算法训练 平方计算
    Java实现 蓝桥杯VIP 算法训练 平方计算
    Java实现 蓝桥杯VIP 算法训练 乘法表
    Java实现 蓝桥杯VIP 算法训练 乘法表
    监管只是压倒网盘业务的一根稻草,但不是主要原因(答案只有一个:成本!)
  • 原文地址:https://www.cnblogs.com/theswiftworld/p/node-object-assign.html
Copyright © 2011-2022 走看看