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
元字符的情况。
9251214
function 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();