Object-Path
今天看到了一个非常有意思的JavaScript库,乍一看有点iOS keypath chain的意思,名字叫Object-Path,地址点我,主要的功能包含了:
- Set
- Get
- Del
- Empty
- Insert
- EnsureExist
- Push
从它的Example中我们可以先一览它的功能:
var obj = {
a: {
b: "d",
c: ["e", "f"],
'\u1200': 'unicode key',
'dot.dot': 'key'
}
};
var objectPath = require("object-path");
//get deep property
objectPath.get(obj, "a.b"); //returns "d"
objectPath.get(obj, ["a", "dot.dot"]); //returns "key"
objectPath.get(obj, 'a.\u1200'); //returns "unicode key"
从这个例子中,我们可以发现对于一个Object obc,除了默认的直接通过点 .方式访问一个属性外, 如obj.a。我们还可以通过点 . 来进行“链式”访问其属性的属性(如果存在的话),如obj.a.b。这看起来真的和iOS中的keypath coding非常相似,所以,今天就让我们来分析下其实现吧!
源码解析
Set方法
例子:objectPath.set(obj, "a.h", "m");
Set方法是用来给obj的某个“链式”属性进行赋值的,源码如下:
function set(obj, path, value, doNotReplace){
// 1.
if (isNumber(path)) {
path = [path];
}
// 2.
if (isEmpty(path)) {
return obj;
}
// 3.
if (isString(path)) {
return set(obj, path.split('.').map(getKey), value, doNotReplace);
}
// 4.
var currentPath = path[0];
if (path.length === 1) {
var oldVal = obj[currentPath];
if (oldVal === void 0 || !doNotReplace) {
obj[currentPath] = value;
}
return oldVal;
}
// 5.
if (obj[currentPath] === void 0) {
//check if we assume an array
if(isNumber(path[1])) {
obj[currentPath] = [];
} else {
obj[currentPath] = {};
}
}
return set(obj[currentPath], path.slice(1), value, doNotReplace);
}
- 判断传入的path是否是Number类型,如果不是的话,构建一个包含这个pathde数组,这里为什么要这样处理,在下文解释。注:JavaScript中不存在浮点数、整数等等不同数值类型,统一为Number
- 判断传入的path是不是“空”,空的情况包含:undefined,空数组,没有任何自身属性的对象。下文我们会详细查看isEmpty的实现。
- 判断是否是字符串,如果是字符串,就通过“.”进行分割,分割完成构建数组,然后进行set方法的重新调用。
- 判断是否当前path深度只为一层,var currentPath = path[0],如果是一层的话,根据是否要doNotReplace进行值的替换。
这里有个很有意思的实现 oldVal === void 0,void 0是什么鬼,我们经常会看见JavaScript::void(0)代表网页的死链接,那么oldVal == void 0又是什么意思呢?
其实,void 0就是undefined!在现代的浏览器中,它们两已经完全的等同了。而写成void 0的形式,是为了兼容。在过去的浏览器中,undefined不是一个关键字,它是个全局变量。因此,你完全可以徒手改变它的含义,
var undefined = 1;
console.log(undefined);
这样的改变,就会undefined原本的含义错乱。而由于void是一个操作符,你无法改写它的含义,因此void 0是一种更安全的写法!
在这里的含义就是,判断oldVal === void 0是不是未定义罢了。
- 如果不是一层深度的path,并且当前的obj[currentPath]是未定义的。那么就需要判断path[1],也就是path数组中的第二个属性是什么了。如果是数字,就将obj[currentPath]构建为数组[],否则构建一个对象{}。
为什么要分开处理呢?这里就需要提及JavaScript访问属性的一个特别的地方。对于如下这样一个对象,
var obj = {a:5}
来说,我们实现obj.a 或者 obj[“a”]都可以获得正确的结果5,但是对于另一个对象,
var obj = {1: 5}
我们只能使用obj[“1”]的方式来访问结果5,而不能使用obj.1。如果使用obj.1,就能报错误:
究其原因,就在于JavaScript在处理对象的key的时候,都是将key当成字符串处理的。
所以,在这里构建完相对应的下一层级obj[currrentPath],递归调用set方法即可。
Del方法
function del(obj, path) {
if (isNumber(path)) {
path = [path];
}
if (isEmpty(obj)) {
return void 0;
}
if (isEmpty(path)) {
return obj;
}
if(isString(path)) {
return del(obj, path.split('.'));
}
// 重点
var currentPath = getKey(path[0]);
var oldVal = obj[currentPath];
if(path.length === 1) {
if (oldVal !== void 0) {
if (isArray(obj)) {
obj.splice(currentPath, 1);
} else {
delete obj[currentPath];
}
}
} else {
if (obj[currentPath] !== void 0) {
return del(obj[currentPath], path.slice(1));
}
}
return obj;
}
让我们再来看看Del方法,这个方法用于“链式”删除一些属性对应的值。由于之前的逻辑和Set类似,我们直接从重点开始看起。
- 首先先从path[0]中获取key,然后取出对应的oldVal。
如果path深度为1,并且oldVal不是未定义的话,究进行删除,删除分为两种:
(1)如果是数组的话,通过splice删除数组中currentPath位置上的属性值
(2)如果是object,直接进行删除即可如果深度不为1,并且oldValue不是未定义,递归调用del函数即可。
这里有一点需要注意,我们看到在获取currentPath的过程中,调用了getKey这个函数,让我们赶紧去看看这个getKey的实现。
function getKey(key){
var intKey = parseInt(key);
if (intKey.toString() === key) {
return intKey;
}
return key;
}
这里的逻辑不难理解,通过parseInt将key转成整型,如果转换后的结果通过toString函数和原有的key一致,就直接返回整形,否则返回原有的key。通过===(强等于号)不难理解,这里if的满足条件当且仅当key本身是string类型,同时其值是个整数才可以满足,如”5”等,像类似”a.2”, “5.5”就不会满足。
剩下的诸如Get, Has方法,实现都大同小异,我们就不再一一解读了,大家有兴趣可以自行阅读。
补充知识:JavaScript类型判断
function isNumber(value){
return typeof value === 'number' || toString(value) === "[object Number]";
}
function isString(obj){
return typeof obj === 'string' || toString(obj) === "[object String]";
}
function isObject(obj){
return typeof obj === 'object' && toString(obj) === "[object Object]";
}
function isArray(obj){
return typeof obj === 'object' && typeof obj.length === 'number' && toString(obj) === '[object Array]';
}
function isBoolean(obj){
return typeof obj === 'boolean' || toString(obj) === '[object Boolean]';
}
在Object-Path的实现中,大量的类型判断工作都是通过如上一些函数来搞定的。有人会问了,判断一个类型为啥要这么麻烦,直接用instanceOf或者typeof不可以吗?
- 用instanceOf是肯定错误的,如果被判断的对象不处于同一个页面,那么instanceOf就肯定失效了。
比如,一个页面(父页面)有一个框架,框架中引用了一个页面(子页面),在子页面中声明了一个array,并将其赋值给父页面的一个变量,这时判断该变量是否是array类型,就是失败
- 用typeof的缺陷在于,JavaScript中存在基本类型和包装类型,对于数值5来说,它是基本类型Number,通过typeof可以准备的判断出。但是如果是包装对象var k = new Number(5),在意义上其仍然应该是属于数值类型,但是通过typeof获得的结果是object,因此就不正确。
所以,在这里的实现,我们采用了Object.prototype.toString.call(obj)的方式来获取正确的类型。
嘿嘿,今天就到这里啦。