【若川视野 x 源码共读】第24期 | vue2工具函数学习笔记

449 阅读9分钟

1.说明

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

2.前言

本文是针对【若川视野 x 源码共读】第24期 | vue2工具函数的阅读总结笔记,该文并不会全部阐述文章中工具函数的使用,只会针对个别本人觉得复杂或者理解不透,亦或是比较耐用的工具函数,进行些许笔墨的分析。如有描述不对、不足的地方,还请及时告知,不吝感谢!

3.工具函数

3.1 freeze()冻结对象

获取一个冻结的空对象,该对象不可变。

var emptyObject = Object.freeze({});

拓展:冻结对象的第一层数据不可变,任何的操作都是无效的,但是第二层,即当第一层是引用数据,其内部的属性数据是可以变更的。

let obj = {
  name: "阿离",
  age: 18,
  family: {
    dad: {
      name: "离爸",
      age: 50,
    },
  },
};

Object.freeze(obj);
console.log(obj); //{ name: '阿离', age: 18, family: { dad: { name: '离爸', age: 50 } } }
obj.name = "西西";
console.log(obj); //{ name: '阿离', age: 18, family: { dad: { name: '离爸', age: 50 } } }
obj.family.age = 60;
console.log(obj); //{ name: '阿离', age: 18, family: { dad: { name: '离爸', age: 60 } } }
Object.defineProperty(obj, "age", {
  value: 20,
}); //报错不能重新定义属性:Cannot redefine property: age

3.2 toString()获取数据原始类型

Object.prototype.toString() 方法返回一个表示该对象的字符串。

var _toString = Object.prototype.toString; 

function toRawType (value) {
    return _toString.call(value).slice(8, -1) 
}

//例子:
Object.prototype.toString();//[object Object]

toRawType('') // 'String' 
toRawType() // 'Undefined'
toRawType(1) // 'Number'
toRawType({age:1}) // 'Object'
toRawType([1]) // 'Array'

过程分析:

  1. 获取Object.prototype.toString方法,赋值给_toString;
  2. 通过call方法调用_toSring()方法,并指定this为需要获取原始类型的数据。toString内部是根据隐式调用获取this,去执行内部的逻辑,因此Object.prototype.toString()得到的值为[object Object];注意String的toString对原型上的toString进行了重写,此处不作再述;
  3. 此处通过call把this显示的传递到toString中;
  4. 通过_toString.call(value)获取的结果形如[object 原始类型],继续通过字符串的slice方法进行截取,从索引为8字符(包含)或者第8个字符(不包含)开始截取,截到倒数第1个(不包含),得到最终的原始类型;

3.3 根据toString()判断纯对象、拓展区分Array和Objcet的几种方法

根据上面的_toSring().call(obj)拿到原始数据类型,判断值是否为[object Object]来确定是否是纯对象。

function isPlainObject (obj) {
    return _toString.call(obj) === '[object Object]' 
}

拓展:区分Array和Objcet的几种方法:

由于valueof针对null/数组/对象等引用类型返回的值都是Objcet(注意Function对象返回的时function) ,因此无法使用valueof区分对象和数组,因此可以采用下面几种方式去区分;

  1. 使用toString():获取数据的原始对象类型,进行区分;
const arr = [1,2,3]
const obj = {name: '阿离'}

Object.prototype.toString.call(obj)==="[object Array]" // false 实际为"[object Object]"
Object.prototype.toString.call(arr)==="[object Array]" // true
  1. 使用Array.isArray():官方推荐
const arr = [1,2,3];
const obj = {name: '阿离'};

Array.isArray(arr) // true
Array.isArray(obj) // false
  1. instanceof:左侧是实例对象,右侧是构造函数,返回值布尔值
const arr = [1,2,3];
const obj = {name: '阿离'};

arr instanceof Array // true 
obj instanceof Array // false
  1. isPrototypeOf():构造函数原型调用isPrototypeOf()方法,接收需要判断的参数 判断参数的原型链上是否有调用的构造函数原型,来判断类型。
const arr = [1,2,3];
const obj = {name: "阿离"};

Array.prototype.isPrototypeOf(arr) // true
Array.prototype.isPrototypeOf(obj) // false

注意:此处不能使用Object.prototype.isPrototypeOf(),因为不论数组还是对象,原型链的顶层都是Object。

  1. construtor构造器:依旧是从原型链追溯 数据.constructor会从原型链追溯,通过__proto__指向上一级的显示原型对象prototype,而prototypeconstructor指向该级的构造函数。
const arr = [1,2,3];
const obj = {name: '阿离'};

arr.constructor === Array;
obj.constructor === Object;

3.4 封装方法isValidArrayIndex判断是否是可用的数组索引值

封装一个函数,接收索引参数,执行逻辑判断该索引参数是否是一个可用的数组的索引,可用的索引是指有限的正整数和0。

function isValidArrayIndex (val) {
    var n = parseFloat(String(val)); 
    return n >= 0 && Math.floor(n) === n && isFinite(val) 
}

//例子
console.log(isValidArrayIndex(0)); //true
console.log(isValidArrayIndex(-1)); //false
console.log(isValidArrayIndex(1.0)); //true
console.log(isValidArrayIndex(1.1)); //false
console.log(isValidArrayIndex({})); //false
console.log(isValidArrayIndex([])); //false
console.log(isValidArrayIndex("")); //false
console.log(isValidArrayIndex(null)); //false
console.log(isValidArrayIndex("abc")); //false
console.log(isValidArrayIndex("234")); //true

说明:

  1. 此方法之所以单独列出来去分析,主要原因是里面涉及到了多种边界情况,这是开发中封装方法所需要具备的思维方式;
  2. 此方法是在js而不是ts写的,所以就需要对参数进行多种情况考量;
  3. 先把参数使用String()转成字符串形式,继续使用parseFloat()转成浮点型,此时除了数值和字符串数值,其余任何数据都会被转成NAN;
  4. 再判断处理后的数据是否大于0,并且Math.floor判断是否是整数,以及isFinite是否有限;

3.5 makeMap 生成一个 map映射对象与Object.create()

接收一个以逗号分隔的字符串,生成一个 map(键值对,注意不是Map对象,是普通的对象),返回值是函数,这个函数用于函数检测参数 key 值在不在这个 map 对象中,并返回该值。第二个参数expectsLowerCase是扩展功能,获取返回值的小写。

function makeMap ( str, expectsLowerCase ) {
    var map = Object.create(null); 
    var list = str.split(','); 
    for (var i = 0; i < list.length; i++) {
        map[list[i]] = true; 
    } 
    return expectsLowerCase ? 
           function (val) {
               return map[val.toLowerCase()];
           } : 
           function (val) {
               return map[val]; 
           } 
}

说明:

1.让我们对代码进行一些改造,我们先去掉expectsLowerCase扩展功能,精简代码;

function makeMap ( str ) {
    var map = Object.create(null); 
    var list = str.split(','); 
    for (var i = 0; i < list.length; i++) {
        map[list[i]] = true; 
    } 
    return function (val) {  return map[val]; } 
}

//获取定制化函数
const strFn = makeMap("a,b,c,d,e");
//此时内部的map对象为:{ a: true, b: true, c: true, d: true, e: true }
const hasA = strFn("a"); //true
const hasNull = strFn(null); //undefined
const hasF = strFn("F"); //undefined
  1. 分析上述精简的代码,使用了Object.create() ,该方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__,详情请移步MDN
  2. 回到代码中,其实该方法采用了闭包的柯里化,执行makeMap函数,接收str参数,返回对应str定制化函数,内部对str参数进行split,创建映射关系的对象;
  3. 后续只需要调用定制化函数,接收键名参数,去映射对象中获取键值; 补充:
  • Object.create的应用场景:由于Object.create(null)创建出来的对象是一个干净对象,除自身属性之外,没有附加其他属性,我们知道for in 或者对象 in时会遍历原型对象prototype上的可枚举属性,如果我们不需要这部分数据,势必就会造成部分性能的损耗,使用Object.create(null)则不必再对其进行遍历了。
  • 手写Object.create:我们粗略的了解了Object.create的用法和用途,现在我们再简单的手写一下Object.create内部是如何实现的;
// 判断是否有create方法
if (typeof Object.create !== "function") { 
    Object.create = function(proto, properties) {
    // 判断类型,第一个参数传入的必须是 object, function
        if (typeof proto !== "object" && typeof proto !== "function") {
            throw new TypeError("Object prototype may only be an Object: " + proto); 
        }
         // 简单的实现的过程,忽略了properties
        var func = function() {}; 
        func.prototype = proto; // 将fn的原型指向传入的proto
        return new func(); //返回构造函数的实例对象
        //此时实例对象的隐式原型__proto__指向构造函数func的显示原型prototype,即指向传入的proto参数
    }
}

简单说明一下new 构造函数的执行过程:

  1. 创建一个空对象obj;
  2. 把构造函数内的this赋值给这个空对象,即obj = this;
  3. 把构造函数的显示原型prototype赋值给空对象的隐式原型__proto__,即obj.__proto__ = 构造函数.prototype;(注意在开发过程中,不要直接使用__proto__,这个属性是不安全的,这个浏览器方便使用获取原型对象而定义的属性,虽然多数浏览器都支持,但是使用该属性是不安全的,因此不要直接使用)
  4. 执行构造函数内的代码逻辑;
  5. 返回该对象,即return obj

3.6 利用闭包特性,缓存数据cached

利用闭包的特性,创建了一个定制缓存数据的函数,同时为了性能等考虑,采用了Object.create,即存储数据的缓存对象是一个干干净净的对象。

function cached (fn) { 
    var cache = Object.create(null); 
    return (function cachedFn (str) {
                var hit = cache[str];
                return hit || (cache[str] = fn(str))
            })
}

分析:

  1. cached方法接收参数fn函数,该函数用于处理存储的键值;
  2. 该方法返回值是函数,形成了闭包,返回的函数接收参数str,作为存取数据的键名;
  3. 执行返回值函数时,根据str参数从cache这个存储对象中获取对应的键值,如果获取不到,则对该数据进行存储,并返回键值,实现存和取功能一体;

3.7 camelize 连字符转小驼峰

封装一个方法camelize,实现输入带连字符-的字符串,处理之后返回值为驼峰字符串。

var camelizeRE = /-(\w)/g;

var camelize = cached(function (str) {
 return str.replace(camelizeRE, function (_, c) {
   return c ? c.toUpperCase() : "";
 });
});

console.log(camelize("on-mousemove")); //onMousemove

分析:

  1. 创建一个正则表达式/-(\w)/g(有不熟悉正则的,可以自行查阅相关资料,此处只针对出现的部分正则内容加以解释),此处为了获取连字符加紧接着的第一个字符串,使用了-(\w),之所以\w使用()分组,是为了方便拿到连字符后面的那个字符串,考虑到可能不止一个连字符,采用了g全局匹配;
  2. 这里还使用了3.6中的缓存数据方法cached,详情请至3.6查看
  3. 为了实现功能,这里采用了replace方法替换,详情查阅MDN,第二个参数为函数,即正则匹配到的内容都会经过这个函数处理,函数接收参数,此处第一个参数_表示匹配到的内容(如:-m),第二个参数c为第一个分组内容,即(\w)对应的内容(如:m)。此处之所以第二个参数不采用字符串+特殊变量的方式,而采用函数方式,是因为采用字符串时,虽然变量能拿到匹配的值,但无法直接处理变量对应的匹配值,只能用于插入,而此处需要转换大小写,故采用函数;
  4. 函数内部把分组中的字符串转成了大写,实现连字符转驼峰;