Object-Path 源码解读

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);
  }
    1. 判断传入的path是否是Number类型,如果不是的话,构建一个包含这个pathde数组,这里为什么要这样处理,在下文解释。注:JavaScript中不存在浮点数、整数等等不同数值类型,统一为Number
    1. 判断传入的path是不是“空”,空的情况包含:undefined,空数组,没有任何自身属性的对象。下文我们会详细查看isEmpty的实现。
    1. 判断是否是字符串,如果是字符串,就通过“.”进行分割,分割完成构建数组,然后进行set方法的重新调用。
    1. 判断是否当前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是不是未定义罢了。

    1. 如果不是一层深度的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,就能报错误:

Uncaught SyntaxError: Unexpected number(…)

究其原因,就在于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)的方式来获取正确的类型。

嘿嘿,今天就到这里啦。