本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
1. 前言
文章主要根据若川大佬【若川视野 x 源码共读】第24期 | vue2工具函数文章的引导,结合自己看代码时的一些理解而写的总结,文章中也存在一些自己还未理解的地方;此外,若文章中有不合理或错误的地方还望各位读者告知,万分感谢。
2. 工具函数
emptyObject
定义空对象
var emptyObject = Object.freeze({})
Object.freeze()冻结对象,冻结后不允许对对象进行任何修改
isUndefined
是否未定义
function isUndef (v) {
return v === undefined || v === null
}
- 包含两种
undefined和null基本数据类型;不包含NaN判断,NaN表示'Not A Number'非基本数据类型 undefined声明但未赋值null变量值为空NaN数据类型转换时,被转换数据不含可以转换的部分
isDefined
是否已定义
function isDef (v) {
return v !== undefined && v !== null
}
- 刚好与上面的判断相反,判断是否不为
undefined和null
isTrue
是否为true
function isTrue (v) {
return v === true
}
- 此处应该是包含了两层判断,
v是否为boolean以及是否为boolean的true
isFalse
是否为false
function isFalse (v) {
return v === false
}
isPrimitive
是否为基本数据类型
function isPrimitive (value) {
return (
typeof value === 'string' ||
typeof value === 'number' ||
// $flow-disable-line
typeof value === 'symbol' ||
typeof value === 'boolean'
)
}
-
判断是否为除
undefined和null外的其余四种基本数据类型string、number、boolean和symbol -
symbol: ES6新增的一个基本数据类型,表示一个独一无二的值,常用来定义对象的唯一属性名let s1 = Symbol("symbol"); // 创建一个symbol实例 let s2 = Symbol("symbol"); s1 === s2; // false /** 对象中的使用 **/ let name1 = Symbol("name"); let name2 = Symbol("name"); let obj = { [name1]:"Jack" } obj1[name2] = "Rose"; //以symbol作为属性不能使用点运算符添加属性,用点来添加属性是添加常规字符串属性 //obj.n = "tom"; console.log(obj); // {Symbol(name): "Jack", Symbol(name): "Rose"} obj[name1]; // Jack obj.name2; // undefined // 不会被for...in遍历 // 不会被Object.keys(obj)、Object.values(obj)和Object.getOwnPropertyNames(obj)返回 // 通过Object.getOwnPropertySymbols()和Reflect.ownKeys()可取到值 console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(name), Symbol(name)] console.log(Reflect.ownKeys(obj)); // [Symbol(name), Symbol(name)]
isObject
是否为对象
function isObject (obj) {
return obj !== null && typeof obj === 'object'
}
-
不为
null且typeof为'object' -
typeof返回具体结果 ,参考自ECMA262Type of val Result Undefined "undefined" Null "object" Boolean "boolean" Number "number" String "string" Symbol "symbol" BigInt "bigint" Object (does not implement [[Call]]) "object" Object (implements [[Call]]) "function"
_toString
定义_toString方法
var _toString = Object.prototype.toString;
Object.prototype指向对象的原型。
toRawType
获取对象的原型类型
function toRawType (value) {
return _toString.call(value).slice(8, -1)
}
toRawType(2)输出NumbertoRawType("2")输出StringtoRawType(Symbol("name"))输出Symbol
isPlainObject
是否为纯粹的对象
function isPlainObject (obj) {
return _toString.call(obj) === '[object Object]'
}
- 原理和上一个方法相同,观察对象的原型类型是否为
Object即可 - 这里要是我写的代码的话可能直接就是
return toRawType(obj) === 'Object',不知道大佬有啥其它的考量
isRegExp
是否为正则表达式
function isRegExp (v) {
return _toString.call(v) === '[object RegExp]'
}
- 和上面一样的对比原型类型,没啥说法
isValidArrayIndex
是否为有效数组索引
function isValidArrayIndex (val) {
var n = parseFloat(String(val));
return n >= 0 && Math.floor(n) === n && isFinite(val)
}
parseFloat(String(val))排除非数字类型,转换为float避免将浮点数转为整数;n >= 0 && Math.floor(n) === n保证n为非负整数,isFinite(n)不为Infinity;顺带试了试,欸Math.floor(Infinity) === Infinity。- MDN关于
Infinity的更多说明
isPromise
是否为Promise对象
function isPromise (val) {
return (
isDef(val) &&
typeof val.then === 'function' &&
typeof val.catch === 'function'
)
}
isDef(val)是否已定义typeof val.then === 'function' && typeof val.catch === 'function'存在then和catch两个方法
toString
转化为字符串
function toString (val) {
return val == null
? ''
: Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
? JSON.stringify(val, null, 2)
: String(val)
}
null转为''空串- 数组或对象(且对象toString方法等于_toString)使用
JSON.stringify()转化 - 其它使用
String()强制转换
toNumber
转化为数字
function toNumber (val) {
var n = parseFloat(val);
return isNaN(n) ? val : n
}
- 使用
parseFloat(val)保证浮点数和整数都能正确转换 - 判断结果是否为
NaN,是则说明原字符串无法转化,返回原字符串
makeMap
创建一个Map对象并返回一个用于验证key是否在Map中的方法
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]; }
}
Object.create(null)创建空对象str.split(',')按,分割原字符串,说明第一个参数期望为一个,分隔的字符串- 遍历分割完的结果数组,
map[list[i]] = true将数组中的字符串作为对象属性值,并将值设置为true;从这里可以看出这里的Map并不等同于JavaScript中的内置对象Map - 返回一个方法,用于查找
key是否在Map中,当第二个参数为true时,将方法会将查询的key全都转化为小写字母
isBuiltInTag
检查是否为内置的Tag
var isBuiltInTag = makeMap('slot,component', true);
- 使用上一函数
makeMap生成检查方法,不区分大小写 slot和component应该是有保留用途吧(大胆猜测,捂脸)
isReservedAttribute
检查是否为保留属性
var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is');
- 依旧是使用
makeMap生成检查方法,但此处区分大小写 key、ref、slot、slot-scope和is从函数名理解了,为Vue保留属性
remove
从数组中删除元素的方法
function remove (arr, item) {
if (arr.length) {
var index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1)
}
}
}
if (arr.length)保证数组不为空arr.indexOf(item)查找元素在数组中的对应下标if (index > -1)元素存在时return arr.splice(index, 1)删除对应元素并返回删除元素的值- Array.splice的更多用法,如:截取和替换
hasOwnProperty
检查对象是否包含属性
var hasOwnProperty = Object.prototype.hasOwnProperty;
Object.prototype.hasOwnProperty该方法返回一个布尔值,只是对象自身是否包含指定属性;方法会忽略从原型链上继承到的属性。
hasOwn
检查指定对象是否包含指定的属性
function hasOwn (obj, key) {
return hasOwnProperty.call(obj, key)
}
- 使用
hasOwnProperty进行判断
cached
创建一个纯函数的缓存版本
function cached (fn) {
var cache = Object.create(null);
return (function cachedFn (str) {
var hit = cache[str];
return hit || (cache[str] = fn(str))
})
}
camelize
将连字符分隔字符串转化为驼峰格式
var camelizeRE = /-(\w)/g;
var camelize = cached(function (str) {
return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; })
});
-(\w)匹配-[a-zA-Z0-9_]格式字符串replace()第二参数传入函数时,函数返回值替换匹配的结果- MDN String.prototype.replace()
capitalize
将字符串首字母转化为大写
var capitalize = cached(function (str) {
return str.charAt(0).toUpperCase() + str.slice(1)
});
str.charAt(0)获取字符串首字母
hyphenate
将驼峰格式转化为连字符分隔字符串
var hyphenateRE = /\B([A-Z])/g;
var hyphenate = cached(function (str) {
return str.replace(hyphenateRE, '-$1').toLowerCase()
});
\B([A-Z])匹配非单词边界的A-Z,即单词中(非单词首尾)的大写字母
polyfillBind
???暂时还没理解是做什么用的函数
/**
* Simple bind polyfill for environments that do not support it,
* e.g., PhantomJS 1.x. Technically, we don't need this anymore
* since native bind is now performant enough in most browsers.
* But removing it would mean breaking code that was able to run in
* PhantomJS 1.x, so this must be kept for backward compatibility.
*/
/* istanbul ignore next */
function polyfillBind(fn, ctx) {
function boundFn(a) {
var l = arguments.length;
return l
? l > 1
? fn.apply(ctx, arguments)
: fn.call(ctx, a)
: fn.call(ctx)
}
boundFn._length = fn.length;
return boundFn
}
function nativeBind(fn, ctx) {
return fn.bind(ctx)
}
var bind = Function.prototype.bind
? nativeBind
: polyfillBind;
看若川大佬的解释是
兼容了老版本浏览器不支持原生的
bind函数。同时兼容写法,对参数的多少做出了判断,使用call和apply实现,据说参数多适合用apply,少用call性能更好。
需要补习下bind和作用域相关知识
toArray
将一个类数组对象转化真正的数组
function toArray (list, start) {
start = start || 0;
var i = list.length - start;
var ret = new Array(i);
while (i--) {
ret[i] = list[i + start];
}
return ret
}
- 两个参数:
list需要转化的对象,start从第几项开始转化 while (i--) { ret[i] = list[i + start]; }遍历转化对象的每一项
extend
属性到目标对象
function extend (to, _from) {
for (var key in _from) {
to[key] = _from[key];
}
return to
}
- 将
from对象的所有属性添加到to对象中
toObject
将数组转化为对象
function toObject (arr) {
var res = {};
for (var i = 0; i < arr.length; i++) {
if (arr[i]) {
extend(res, arr[i]);
}
}
return res
}
- 注意因为使用
extend进行转化,所以如果数组是字符串数组后面的字符串的每个字符会覆盖前一项对应索引的项,所以有点不太理解这个函数的作用,貌似只能转化正确只有一项的数组
noop
空函数
/**
* Perform no operation.
* Stubbing args to make Flow happy without leaving useless transpiled code
* with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/).
*/
function noop (a, b, c) {}
- 我不李姐.jpg
no
永假函数(我这样称呼它)
var no = function (a, b, c) { return false; };
- 如其名,永远返回
false
identity
返回自身
var identity = function (_) { return _; };
- 函数返回传入的参数自身
genStaticKeys
生成静态属性
function genStaticKeys (modules) {
return modules.reduce(function (keys, m) {
return keys.concat(m.staticKeys || [])
}, []).join(',')
}
- 不太理解函数的作用,函数基本是理解了,就是将对象数组中的每项的
staticKeys属性值拼成一个,号分隔的字符串
looseEqual
宽松相等
function looseEqual (a, b) {
if (a === b) { return true }
var isObjectA = isObject(a);
var isObjectB = isObject(b);
if (isObjectA && isObjectB) {
try {
var isArrayA = Array.isArray(a);
var isArrayB = Array.isArray(b);
if (isArrayA && isArrayB) {
return a.length === b.length && a.every(function (e, i) {
return looseEqual(e, b[i])
})
} else if (a instanceof Date && b instanceof Date) {
return a.getTime() === b.getTime()
} else if (!isArrayA && !isArrayB) {
var keysA = Object.keys(a);
var keysB = Object.keys(b);
return keysA.length === keysB.length && keysA.every(function (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
}
}
-
当两值严格相等时返回
true -
当两值不严格相等时,分下面几种情况:
-
两值都为
Object时,情况比较多,逐一讨论- 均为数组时:长度相等,且数组对应每项均宽松相等返回
true,否则返回false - 任一不为数组但均继承
Date对象时,getTime()返回结果严格相等则返回true,否则false - 均不为数组时,两对象属性数量相同,且对应属性值宽松相等返回
true,否则返回false - 在不满足上述三种情况时返回
false
- 均为数组时:长度相等,且数组对应每项均宽松相等返回
-
两值都不为
Object时,转化为String两值严格相等,返回true -
上述都不满足,返回
false
-
-
instanceof用于检测构造函数的prototype属性是否出现在某个实例对象原型链上 MDN
looseIndexOf
宽松indexOf
/**
* Return the first index at which a loosely equal value can be
* found in the array (if value is a plain object, the array must
* contain an object of the same shape), or -1 if it is not present.
*/
function looseIndexOf (arr, val) {
for (var i = 0; i < arr.length; i++) {
if (looseEqual(arr[i], val)) { return i }
}
return -1
}
- 返回数组
arr中第一个与目标val宽松相等的值的索引,未找到返回-1,indexOf()的宽松相等版本
once
确保一个方法只被调用一次
function once (fn) {
var called = false;
return function () {
if (!called) {
called = true;
fn.apply(this, arguments);
}
}
}
- 添加
called标志位保证传入后被转化的函数只能执行一次
3. 总结
通过查看源码自己也学到了很多新的知识,还能复习巩固下已掌握的知识,顺便还能学习优秀的编码习惯,一石三鸟;同时也暴露出了自己很多薄弱的点,在阅读源码时只是浅尝辄止还未深挖,但其中如noop方法注释中关于Flow方法调用检查的内容单独也可以总结为一篇学习笔记了,之后可以看下这方面的相关知识,再立一个发篇学习笔记的flag。
\