github仓库地址 在线地址
点击在线地址查看,会发现该文件实际上有很多函数。实际上就是Vue2的工具函数库。下面就来简单学习一下。因为源码用的是ts,理解起来可能会加点成本,所以下面讲解会把类型部分去掉(其实是本人的ts水平不高,很难很好的解释)
const emptyObject = Object.freeze({});Object.freeze()方法可以冻结一个对象。如果对象被冻结后,就不能再被修改、不能添加新的属性、不能删除已有属性、不能修改已有属性的配置(可枚举性、可写性等)。
const freezeObj = Object.freeze({ name: 'clz'});console.log(freezeObj); // { name: 'clz' }// 因为被冻结了,所以修改属性、删除属性都没有效果freezeObj.age = 21;console.log(freezeObj); // { name: 'clz' }delete freezeObj.name;console.log(freezeObj); // { name: 'clz' }冻结对象后,只是第一层无法修改,第二层还是能够修改滴
const freezeObj = Object.freeze({ job: { type: 'Coder', salary: -111 }});console.log(freezeObj); // { job: { type: 'Coder', salary: -111 } }freezeObj.job.salary = 111;console.log(freezeObj); // { job: { type: 'Coder', salary: 111 } }delete freezeObj.job.salary;console.log(freezeObj); // { job: { type: 'Coder' } }// 冻结只会影响第一层。所以给第一层添加属性还是会没有效果freezeObj.name = 'clz';console.log(freezeObj); // { job: { type: 'Coder' } }我们可以通过Object.isFrozen来判断对象是不是冻结状态。
const freezeObj = Object.freeze({ name: 'clz'});console.log(Object.isFrozen(freezeObj)); // trueconsole.log(Object.isFrozen({})); // false那么这个工具库的这个不是函数的变量有什么作用呢?
我知道的场景就是通过赋值冻结的空对象,防止不小心添加属性等操作,比较程序员有可能手误。(这里想要感谢一下若川大佬,评论问问题,很耐心地解答)
判断是不是没有定义。
function isUndef(v) { return v === undefined || v === null}注意:这个函数名是叫isUndef,但是实际上,如果参数是null的话,因为null也没有什么实际意义,所以把null和undefined捆绑了(个人感觉此处命名有点点瑕疵)。
判断是不是已经定义了。
function isDef(v) { return v !== undefined && v !== null}这里其实就只是上面的取反就行了。因为定义和未定义本就是相反的。
判断是不是true。
function isTrue(v) { return v === true}判断是不是false。
function isTrue(v) { return v === true}判断是不是原始值,即原始类型的值。
function isPrimitive(value) { return ( typeof value === 'string' || typeof value === 'number' || // $flow-disable-line typeof value === 'symbol' || typeof value === 'boolean' )}JS的基础类型有:
string
number
boolean
undefined
null
Symbol
BigInt
undefined和null已经在前面的未定义、已定义那里切出去了,而BigInt是ES2020新增的基本类型。这里的isPrimitive更像是判断是不是有用的原始类型。
判断是不是对象。
function isObject(obj) { return obj !== null && typeof obj === 'object'}因为typeof null的结果也是object,所以还需要确定不是null才行。
这里简单讲一下原因:JS存储数据用的是二进制存储,数据的前三位是存储的类型。对象的前三位是000,而null则是全为0,即前三位也是000。所以typeof null也是object。
判断是不是纯对象。
function isPlainObject(obj) { return _toString.call(obj) === '[object Object]'}上面的_toString实际上是Object.prototype.toString,因为比较常用,所以处理成_toString变量。
isObject只是判断是不是对象,但是实际上用来判断数组也能得到true的结果,因为数组也是对象。所以还得有一个判断是不是纯对象的方法。而判断的方法也比较简单,只需要调用Object.prototype.toString方法即可(要使用bind方法来调用)。如果是数组得到的结果会是'[object Array]',而纯对象得到的结果是'[object Object]'。

判断是不是正则表达式
function isRegExp(v) { return _toString.call(v) === '[object RegExp]'}判断是不是有效的数组索引值。
function isValidArrayIndex(val) { const n = parseFloat(String(val)) return n >= 0 && Math.floor(n) === n && isFinite(val)}第一步是将参数变成字符串,第二步是转成浮点数。(第一行)
第二行是重点:
n >= 0,因为数组索引值不能是负数
Math.floor(n) === n,因为数组索引值不能是负数
isFinite(val,参数只能是有限数值。在必要情况下,参数会转换称为数值。
下面稍微举几个例子方便理解isFinite。其实也没啥好说的,就是限制参数只能是有限数值。顺带提一嘴,实际上第一行已经和前两个条件已经能过滤掉一些情况了,因为像是NaN,true这些参数,转成字符串再转成浮点数就已经是NaN了。
console.log(isFinite(Infinity)); // falseconsole.log(isFinite(NaN)); // falseconsole.log(isFinite(-Infinity)); // falseconsole.log(isFinite('12345a')); // falseconsole.log(isFinite('0')); // trueconsole.log(isFinite('123')); // trueconsole.log(isFinite(123)); // trueconsole.log(isFinite(-123)); // trueconsole.log(isFinite(true)); // trueconsole.log(isFinite(0b101)); // trueconsole.log(isFinite(0xFF)); // trueconsole.log(isFinite(1.234)) // true判断是不是Promise对象。这个方法非常有效,并且有意思。判断是不是Promise并不是直接判断,而是通过判断它的then和catch属性是不是函数来间接判断是不是Promise对象
function isPromise() { return ( isDef(val) && typeof val.then === 'function' && typeof val.catch === 'function' )}转换成原始类型(得到参数的类型)
function isPromise() { return ( isDef(val) && typeof val.then === 'function' && typeof val.catch === 'function' )}其实就是借助前面提到过的Object.prototype.call(value)能得到形似[object Type]的字符串。比较精确,如数组也是对象,通过这个方法能得到是数组,而不只是对象。然后通过slice(8, -1)把参数的类型部分拿到。

转换成字符串。
function toString(val) { return val == null ? '' : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString) ? JSON.stringify(val, null, 2) : String(val)}首先,原始类型通过String()方法就能直接转换成对应的字符串,但是undefined和null转换成字符串应该是空串才更合理,所以上面用了普通的==来判断是不是这两个值,如果是,则返回空串。
但是,当值是对象、数组的时候,结果会有点差强人意。
const obj = { name: 'clz'};const arr = [1, 2, 3, 4];console.log(String(obj)); // [object Object]console.log(String(arr)); // 1,2,3,4这时候就需要使用JSON.stringify()来将它们转换成对象了,可以看一下之前写的笔记JSON的使用之灵活版 | 赤蓝紫
const obj = { name: 'clz'};const arr = [1, 2, 3, 4];console.log(JSON.stringify(obj)); // {"name":"clz"}console.log(JSON.stringify(arr)); // [1,2,3,4]至于源码中的第三个参数,其实就只是指定缩进的空格是2个,用于美化输出的。
转换成数字型,如果没法转换成数字型就返回原字符串。该方法参数只能是字符串类型。
function toNumber(val) { const n = parseFloat(val) return isNaN(n) ? val : n}
注意,由于parseFloat只要参数字符串的第一个字符能被解析成数字,就会返回数字,即使后面不是数字也一样,如上面例子的123a。
另外Infinity也能被解析并返回Infinity。
将伪数组转换成真数组。第二个参数可选,可以控制真数组的开始位置,默认是0。
function toArray (list, start) { start = start || 0 let i = list.length - start const ret = new Array(i) while (i--) { ret[i] = list[i + start] } return ret}伪数组具有length属性,但是不具备数组的push、forEach等方法。
9251214扩展对象,把一个对象的属性值扩展到另一个对象上。放在这里主要是后面的toObject会使用到。
function extend(to, _from) { for (const key in _from) { to[key] = _from[key] } return to}注意,如果to上也有_from的属性,那么_from的该属性值会覆盖掉to上的。

将一个对象数组合并到另一个对象中去。
function toObject(arr) { const res = {} for (let i = 0; i < arr.length; i++) { if (arr[i]) { extend(res, arr[i]) } } return res}
注意:上面例子中,最后生成的对象不存在之前的456,这是因为456不能被for in遍历,而Hello能被遍历,只是会被拆解。不过,该方法用法应该只是将数组里的对象合并到另一个对象中去(从注释猜测的)
主要介绍makeMap方法以及使用makeMap方法的。
生成一个map,注意:这里的map只是键值对形式的对象。并且返回的并不是生成的map,而是一个函数,用来判断key在不在map中的对象。具体可以实例可以查看下面的isBuiltInTag的。expectsLowerCase是可选参数,表示会将字符串参数变为小写,即不区分大小写。
function makeMap ( str, expectsLowerCase) { const map = Object.create(null) const list = str.split(',') for (let i = 0; i < list.length; i++) { map[list[i]] = true } return expectsLowerCase ? val => map[val.toLowerCase()] : val => map[val]}这个方法并没有很复杂。简单讲一下步骤:
Object.create(null)生成没有原型链的空对象
str.split(',')把字符串以,为分隔符,将字符串分割为字符串数组
key,以true为值添加到map中,表示该key在生成的map中。
key在不在map中。这里会判断第二个参数是不是true,如果是,则不区分大小写。
判断是不是内置的tag(这里的内置并不是html的标签,而是Vue的slot和component)。用的是上面的makeMap方法。
const isBuiltInTag = makeMap('slot,component', true)
上面第二个参数为true,表示不区分大小写,也可以不传,从而区分大小写。

判断是不是保留的属性。还是通过makeMap方法来实现,和isBuiltInTag原理一样,就不再介绍了。
const isReservedAttribute = makeMap('key,ref,slot,slot-scope,is')从数组中删除指定元素。如果有多个指定元素,只删除第一个。
function remove(arr, item) { if (arr.length) { const index = arr.indexOf(item) if (index > -1) { return arr.splice(index, 1) } }}原理就是通过indexOf找到要删除的元素的位置,然后通过splice()方法删除掉该元素。

判断是不是自己的属性,而不是继承过来的。
其实通过obj.hasOwnProperty(key)好像就可以了,但是还是封装成了一个方法。本质应该和isTrue一样,更适合大型项目的开发,比如代码易读性之类的。(猜的,没做过很大型的项目,泪目)
const hasOwnProperty = Object.prototype.hasOwnPropertyfunction hasOwn(obj, key) { return hasOwnProperty.call(obj, key)}9251214利用闭包的特性,缓存数据。
function cached(fn) { const cache = Object.create(null) return (function cachedFn(str) { const hit = cache[str] return hit || (cache[str] = fn(str)) })}接受一个函数,返回一个函数,返回的函数会判断有没有缓存数据,如果有,则直接返回缓存数据,如果没有,才会调用传入的函数,并且会缓存数据。
连字符转驼峰,如on-click转成onClick
const camelizeRE = /-(\w)/gconst camelize = cached(str => { return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')})上面用了正则表达式比较巧妙的实现:
/-(\w)/g会匹配像是-click之类的
replace()替换掉匹配的部分,其中,第二个参数可以是函数,在上面的例子中,该函数的第二个参数代表第1个括号匹配的字符串,即click,然后让它首字母大写。如果没有匹配到的,则不变化。
cached来优化。
首字母大写。
const capitalize = cached(str => { return str.charAt(0).toUpperCase() + str.slice(1)})驼峰转连字符,如onClick转成 on-click
const hyphenateRE = /\B([A-Z])/gconst hyphenate = cached(str => { return str.replace(hyphenateRE, '-$1').toLowerCase()})这个原理其实和上面的连字符一样,还简单了一点,因为都是小写字母。
原理就是通过正则表达式去匹配字符串,使用括号和$1实现将匹配到的括号内的字符串变成添加上-的形式。最后再将整个字符串小写化。
那么\B有什么用呢?
\B元字符匹配非单词边界。匹配位置的上一个和下一个字符的类型是相同的,即必须同时是单词,或同时是非单词字符。字符串的开头和结尾处被视为非单词字符。
所以当大写字母在字符串开头时不会被转化,即Onclick不会变成-onclick。

顺便看下没有\B元字符的情况。
9251214function noop(a, b, c) {} // 参数都是可选的第一反应:???
后面查了下资料:noop的主要作用是为一些函数提供默认值,避免传入undefined之类的数据导致代码出错。即如果参数原本是函数,但是最后传了undefined的话,就会报xx is not a function的错。
9251214永假函数。
const no = (a, b, c) => false // 参数都是可选的返回自身。
const identity = (_) => _生成静态键的字符串。
function genStaticKeys(modules) { return modules.reduce((keys, m) => { return keys.concat(m.staticKeys || []) }, []).join(',')}接收一个对象数组,将staticKeys的值(数组),拼接成静态键的数组,最后将该数组转化成字符串形式,用,连接。

宽松相等:两个对象(包括数组)比较,如果它们形状相同,就返回true。
{}和另一个{}是不相等的,因为对象是引用类型,但是用looseEqual来判断是会认为相等的,因为形状(内容)完全相同。
function looseEqual (a: any, b: any): boolean { if (a === b) return true const isObjectA = isObject(a) const isObjectB = isObject(b) if (isObjectA && isObjectB) { try { const isArrayA = Array.isArray(a) const isArrayB = Array.isArray(b) if (isArrayA && isArrayB) { return a.length === b.length && a.every((e, i) => { return looseEqual(e, b[i]) }) } else if (a instanceof Date && b instanceof Date) { return a.getTime() === b.getTime() } else if (!isArrayA && !isArrayB) { const keysA = Object.keys(a) const keysB = Object.keys(b) return keysA.length === keysB.length && keysA.every(key => { return looseEqual(a[key], b[key]) }) } else { /* istanbul ignore next */ return false } } catch (e) { /* istanbul ignore next */ return false } } else if (!isObjectA && !isObjectB) { return String(a) === String(b) } else { return false }}流程:
a严格等于b直接返回true
a和b都是对象(包括数组),依次执行以下操作:
every+looseEqual判断数组元素是否都宽松相等
Date对象,那就判断两者的绝对是件是否相同
a和b的键,判断数组长度是否相等,并通过every+looseEqual判断数组元素是否都宽松相等
false。
false,此时是a和b一个是对象,一个不是对象,所以肯定不等。

返回数组中第一个与参数宽松相等的元素的位置。原生的indexOf是严格相等。
function looseIndexOf(arr, val) { for (let i = 0; i < arr.length; i++) { if (looseEqual(arr[i], val)) return i } return -1}
确保函数只执行一次。
function looseIndexOf(arr, val) { for (let i = 0; i < arr.length; i++) { if (looseEqual(arr[i], val)) return i } return -1}主要还是利用闭包缓存数据的特性,定义一个初始值为false的变量,返回一个函数,该函数会判断缓存的数据called是不是false,如果是,则将called变为true,并执行函数,通过apply调用来绑定上下文。
function once(fn) { let called = false console.log(this); return function () { if (!called) { called = true console.log(this); fn.apply(this, arguments) // fn(); } }}const obj = { age: 21 };window.age = 100;obj.fn = once(function () { console.log(this.age);});obj.fn();兼容老版本浏览器不支持原生的bind函数。并且会根据参数的多少来确定使用使用apply还是call,如果参数数量大于1,则使用apply,如果参数数量小于1,则使用call。
function once(fn) { let called = false console.log(this); return function () { if (!called) { called = true console.log(this); fn.apply(this, arguments) // fn(); } }}const obj = { age: 21 };window.age = 100;obj.fn = once(function () { console.log(this.age);});obj.fn();