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'
过程分析:
- 获取
Object.prototype.toString方法,赋值给_toString; - 通过call方法调用_toSring()方法,并指定this为需要获取原始类型的数据。toString内部是根据隐式调用获取this,去执行内部的逻辑,因此
Object.prototype.toString()得到的值为[object Object];注意String的toString对原型上的toString进行了重写,此处不作再述; - 此处通过call把this显示的传递到toString中;
- 通过
_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区分对象和数组,因此可以采用下面几种方式去区分;
- 使用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
- 使用Array.isArray():官方推荐
const arr = [1,2,3];
const obj = {name: '阿离'};
Array.isArray(arr) // true
Array.isArray(obj) // false
- instanceof:左侧是实例对象,右侧是构造函数,返回值布尔值
const arr = [1,2,3];
const obj = {name: '阿离'};
arr instanceof Array // true
obj instanceof Array // false
- isPrototypeOf():构造函数原型调用isPrototypeOf()方法,接收需要判断的参数 判断参数的原型链上是否有调用的构造函数原型,来判断类型。
const arr = [1,2,3];
const obj = {name: "阿离"};
Array.prototype.isPrototypeOf(arr) // true
Array.prototype.isPrototypeOf(obj) // false
注意:此处不能使用Object.prototype.isPrototypeOf(),因为不论数组还是对象,原型链的顶层都是Object。
- construtor构造器:依旧是从原型链追溯
数据.constructor会从原型链追溯,通过__proto__指向上一级的显示原型对象prototype,而prototype的constructor指向该级的构造函数。
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
说明:
- 此方法之所以单独列出来去分析,主要原因是里面涉及到了多种边界情况,这是开发中封装方法所需要具备的思维方式;
- 此方法是在js而不是ts写的,所以就需要对参数进行多种情况考量;
- 先把参数使用
String()转成字符串形式,继续使用parseFloat()转成浮点型,此时除了数值和字符串数值,其余任何数据都会被转成NAN; - 再判断处理后的数据是否大于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
- 分析上述精简的代码,使用了
Object.create(),该方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__,详情请移步MDN; - 回到代码中,其实该方法采用了闭包的柯里化,执行
makeMap函数,接收str参数,返回对应str的定制化函数,内部对str参数进行split,创建映射关系的对象; - 后续只需要调用
定制化函数,接收键名参数,去映射对象中获取键值; 补充:
- 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 构造函数的执行过程:
- 创建一个空对象obj;
- 把构造函数内的this赋值给这个空对象,即obj = this;
- 把构造函数的显示原型
prototype赋值给空对象的隐式原型__proto__,即obj.__proto__=构造函数.prototype;(注意在开发过程中,不要直接使用__proto__,这个属性是不安全的,这个浏览器方便使用获取原型对象而定义的属性,虽然多数浏览器都支持,但是使用该属性是不安全的,因此不要直接使用) - 执行构造函数内的代码逻辑;
- 返回该对象,即
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))
})
}
分析:
cached方法接收参数fn函数,该函数用于处理存储的键值;- 该方法返回值是函数,形成了闭包,返回的函数接收参数
str,作为存取数据的键名; - 执行返回值函数时,根据
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
分析:
- 创建一个正则表达式
/-(\w)/g(有不熟悉正则的,可以自行查阅相关资料,此处只针对出现的部分正则内容加以解释),此处为了获取连字符加紧接着的第一个字符串,使用了-(\w),之所以\w使用()分组,是为了方便拿到连字符后面的那个字符串,考虑到可能不止一个连字符,采用了g全局匹配; - 这里还使用了3.6中的缓存数据方法cached,详情请至3.6查看;
- 为了实现功能,这里采用了
replace方法替换,详情查阅MDN,第二个参数为函数,即正则匹配到的内容都会经过这个函数处理,函数接收参数,此处第一个参数_表示匹配到的内容(如:-m),第二个参数c为第一个分组内容,即(\w)对应的内容(如:m)。此处之所以第二个参数不采用字符串+特殊变量的方式,而采用函数方式,是因为采用字符串时,虽然变量能拿到匹配的值,但无法直接处理变量对应的匹配值,只能用于插入,而此处需要转换大小写,故采用函数; - 函数内部把分组中的字符串转成了大写,实现连字符转驼峰;