第二期笔记: vue3 工具函数
Σ(っ°Д°;)っ 源码阅读
这是若川大佬源码共读活动的第二期 附上链接
打开 vue-next/packages/shared/src/index.ts
1. EMPTY_OBJ 空对象:
源码:
export const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__
? Object.freeze({})
: {}
记录:
DEV :
这是一个伪全局变量 ,用于管理开发环境中需运行的代码块,这在编译阶段会被内联,在 CommonJS
构建中,转化成 process.env.NODE_ENV !== 'production'
这样的判断,用于判断当前环境是开发环境还是生产环境。
Object.freeze():
MDN上描述,这个方法可以冻结一个对象。一个被冻结的对象再也不能被修改;被冻结对象自身的所有属性都不可能以任何方式被修改。此外,冻结一个对象后该对象的原型也不能被修改。freeze()
返回和传入的参数相同的对象。
测试:
const teacher = {
name: 'tom',
students: ['amy'],
age: 40,
feature: {
face: 'big'
}
}
const returnObj = Object.freeze(teacher);
console.log(returnObj === teacher); //true
teacher.age=30;
console.log(teacher.age) //40 没有改变
teacher.students=[];
console.log(teacher.students)//[ 'amy' ] 没有改变
teacher.students.push('jack')
console.log(teacher.students) //[ 'amy', 'jack' ] 成功添加了
teacher.feature.nose='small'
console.log(teacher.feature) //{ face: 'big', nose: 'small' } 成功添加了
结论:冻结的是基础类型以及引用类型的引用,不能修改基础类型的值和引用类型的引用地址,但是可以修改引用地址对象的值。
若要使对象不可变,需要递归冻结每个类型为对象的属性(深冻结),类似深拷贝,同时也要注意循环引用,下面MDN的例子没有做循环引用的处理:
function deepFreeze(obj) {
// 取回定义在obj上的属性名
const propNames = Object.getOwnPropertyNames(obj);
// 在冻结自身之前冻结属性
propNames.forEach((name)=> {
const prop = obj[name];
// 如果prop是个对象,冻结它
if (typeof prop == 'object' && prop !== null)
deepFreeze(prop);
});
// 冻结自身(no-op if already frozen)
return Object.freeze(obj);
}
deepFreeze(teacher);
teacher.feature.mouth='small';
console.log(teacher.feature.mouth);//undefined
对比:
Object.seal()
方法封闭一个对象,阻止添加新属性并将所有现有属性变的不可删除,以及一个数据属性不能被重新定义成为访问器属性,只要原来属性是可写的,值就能修改。
使用Object.freeze()
冻结的对象中现有属性是不可变的。
2. EMPTY_ARR 空数组
源码:
export const EMPTY_ARR = __DEV__ ? Object.freeze([]) : []
测试:
const arr=Object.freeze([]);
arr.push('a') //报错 TypeError: Cannot add property 0, object is not extensible
3.EMPTY_ARR 空数组
源码
export const NOOP = () => {}
这种写法广泛使用,箭头函数比用function(){}更简洁,代码压缩更方便
4.NO 永远返回 false 的函数
源码:
export const NO = () => false
就是一个永远返回false的匿名函数,方便压缩代码。
5. isOn 判断字符串是不是 on 开头,并且 on 后首字母不是小写字母
源码:
const onRE = /^on[^a-z]/
export const isOn = (key: string) => onRE.test(key)
正则表达式:/on[a-z]/,匹配on开头的字符串,并且on后首字母不是小写字母。
测试:
const onRE = /^on[^a-z]/;
const isOn = (key) => onRE.test(key);
console.log(isOn('onChange'));; // true
console.log(isOn('onchange'));; // false
console.log(isOn('on3change'));; // true
6.isModelListener 监听器
源码:
export const isModelListener = (key: string) => key.startsWith('onUpdate:')
判断字符串是不是以onUpdate:
开头,返回布尔类型
测试:
const isModelListener = (key) => key.startsWith('onUpdate:');
console.log(isModelListener('onUpdate:change'));; // true
console.log(isModelListener('1onUpdate:change'));; // false
startswith是es6新增的字符串方法,类似的有:
- includes() :返回布尔值,表示是否找到了参数字符串。
- endsWith() :返回布尔值,表示参数字符串是否在原字符串的尾部。
测试:
console.log('i am jack'.endsWith('jak'));// false
console.log('i am jack'.endsWith('jack'));//true
console.log('i am jack'.includes('am'));//true
console.log('i am jack'.includes('am2'));//false
console.log('i am jack'.indexOf('am'));//2
这三个方法都支持第二个参数,表示开始搜索的位置。
let s = 'Hello world!';
s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 6) // false
上面代码表示,使用第二个参数n
时,endsWith
的行为与其他两个方法有所不同。它针对前n
个字符,而其他两个方法针对从第n
个位置直到字符串结束,即endsWith
的n参数,表示保留前n个字符,按保留的字符算,进行末尾配对。而其他两个是用第n个后的字符串进行配对。
7.extend 合并
源码:
export const extend = Object.assign
Object.assign(target, ...sources)
参数:
target:目标对象。
sources:源对象。
返回值:
返回目标对象。
MDN描述:
如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。后面的源对象的属性将类似地覆盖前面的源对象的属性。
简单来说,就是合并对象属性,后面对象的属性会覆盖前面对象的属性。
注意点:
Object.assign
方法只会拷贝源对象自身的并且可枚举的属性到目标对象。该方法使用源对象的[[Get]]
和目标对象的[[Set]]
,所以它会调用相关 getter 和 setter。因此,它分配属性,而不仅仅是复制或定义新的属性。如果合并源包含getter,这可能使其不适合将新属性合并到原型中。为了将属性定义(包括其可枚举性)复制到原型,应使用Object.getOwnPropertyDescriptor()
和Object.defineProperty()
。
String
类型和 Symbol
类型的属性都会被拷贝。
在出现错误的情况下,例如,如果属性不可写,会引发TypeError
,如果在引发错误之前添加了任何属性,则可以更改target
对象。
Object.assign
不会在那些source
对象值为 null
或 undefined
的时候抛出错误。
深拷贝问题
针对深拷贝,需要使用其他办法,因为 Object.assign()
拷贝的是(可枚举)属性值。
假如源值是一个对象的引用,它仅仅会复制其引用值。
测试
const obj = Object.create({foo: 1}, { // foo 是个继承属性。
bar: {
value: 2 // bar 是个不可枚举属性。
},
baz: {
value: 3,
enumerable: true // baz 是个自身可枚举属性。
}
});
const copy = Object.assign({}, obj);
console.log(copy); // { baz: 3 }
Object.create()
是创建一个对象,第一个参数是新对象的原型对象,第二个参数是新对象自身的属性。
8.remove 移除数组的一项
源码:
export const remove = <T>(arr: T[], el: T) => {
const i = arr.indexOf(el)
if (i > -1) {
arr.splice(i, 1)
}
}
通过数组方法indexof找到目标的索引,然后通过splice进行删除数组的那一项。
splice,是一个很耗性能的方法。删除数组中的一项,其他元素都要移动位置
对于移除数组项,axios的拦截器的处理是把拦截器置为 null
。而不是用splice
移除。最后执行时为 null
的不执行。
axios处理示例:
this.handlers = [];
// 移除
if (this.handlers[id]) {
this.handlers[id] = null;
}
// 执行
if (h !== null) {
fn(h);
}
类似delete
测试:
let arr=[1,3,2]
delete arr[arr.indexOf(3)]
console.log(arr[1]) //undefined
console.log(arr.length)//3
console.log(arr)//[1,undefined,2]
9.hasOwn 是不是自己本身所拥有的属性
源码:
const hasOwnProperty = Object.prototype.hasOwnProperty
export const hasOwn = (
val: object,
key: string | symbol
): key is keyof typeof val => hasOwnProperty.call(val, key)
hasOwnProperty方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键),而不会查找其原型链,通常 for...in 方法会继承的属性及自定义原型属性也遍历,而通过hasOwnProperty方法可以过滤这些属性
示例:
var triangle = {a: 1, b: 2, c: 3};
function ColoredTriangle() {
this.color = 'red';
}
ColoredTriangle.prototype = triangle;
var obj = new ColoredTriangle();
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) {
console.log(`obj.${prop} = ${obj[prop]}`);// "obj.color = red"
}
}
由于源码中的hasOwnProperty方法中,调用hasOwnProperty的是Object构造函数,而不是示例对象,所以需要call函数,将this绑定到需要判断属性的对象中。
10.isArray 判断数组
源码:
export const isArray = Array.isArray
当检测Array实例时, Array.isArray
优于 instanceof
,因为Array.isArray能检测iframes
.
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
xArray = window.frames[window.frames.length-1].Array;
var arr = new xArray(1,2,3); // [1,2,3]
// Correctly checking for Array
Array.isArray(arr); // true
// Considered harmful, because doesn't work though iframes
arr instanceof Array; // false
并instanceof
是通过原型链判断的,如果一个对象的原型指向了Array.prototype,同样会返回true,而isArray不会
const isArray = Array.isArray;
isArray([]); // true
const fakeArr = { __proto__: Array.prototype, length: 0 };
isArray(fakeArr); // false
fakeArr instanceof Array; // true
11.isMap ,isSet
源码:
export const isMap = (val: unknown): val is Map<any, any> =>
toTypeString(val) === '[object Map]'
export const isSet = (val: unknown): val is Set<any> =>
toTypeString(val) === '[object Set]'
export const objectToString = Object.prototype.toString
export const toTypeString = (value: unknown): string =>
objectToString.call(value)
就是通过Object构造函数的toString方法判断类型,如果是map对象,toString方法会打印出'[object Map]',如果是Array对象,
则是'[object Array]' ,如果是set对象,则是'[object Set]',如果是date 属性,则是[object Date]。
测试:
let arr=[];
let map=new Map();
let date=new Date();
let date1={__proto__: new Date()}
console.log(Object.prototype.toString.call(arr));//[object Array]
console.log(Object.prototype.toString.call(map));//[object Map]
console.log(Object.prototype.toString.call(date));//[object Date]
console.log(Object.prototype.toString.call(date1));//[object Object]
可以通过 toString()
来获取每个对象的类型,会输出[object type]。为了每个对象都能通过 Object.prototype.toString()
来检测,需要以 Function.prototype.call()
或者 Function.prototype.apply()
的形式来调用,传递要检查的对象作为第一个参数,称为 thisArg
。
12.toRawType 对象转字符串 截取后几位
源码:
export const toRawType = (value: unknown): string => {
// extract "RawType" from strings like "[object RawType]"
return toTypeString(value).slice(8, -1)
}
截取字符串中的[8,-1],-1指的是最后一位,区间左闭右开,其实是把[object type] 中的type截取出来返回。
13. isIntegerKey 判断是不是数字型的字符串key值
export const isIntegerKey = (key: unknown) =>
isString(key) &&
key !== 'NaN' &&
key[0] !== '-' &&
'' + parseInt(key, 10) === key
首先判断是否是字符串,对象的键都是字符串形式存在的,先判断是否是字符串,然后判断是否是NaN,因为''+parseInt('NAN',10)='NaN'为true,所以要先过滤,然后判断key[0]!=='-'是因为''+parseInt('0',10)==='0'为true,所以也要过滤
parseInt 第二个参数是进制。
14.cacheStringFunction 缓存
源码:
const cacheStringFunction = <T extends (str: string) => string>(fn: T): T => {
const cache: Record<string, string> = Object.create(null)
return ((str: string) => {
const hit = cache[str]
return hit || (cache[str] = fn(str))
}) as any
}
测试:
const cacheStringFunction = (fn) => {
const cache = Object.create(null);
return ((str) => {
const hit = cache[str];
console.log(cache)
return hit || (cache[str] = fn(str));
});
};
function test(str) {
return str
};
let cache=cacheStringFunction(test);
cache('test1');//[Object: null prototype] {}
cache('test2');//[Object: null prototype] { test1: 'test1' }
将函数传入,返回一个函数,返回函数的参数作为第一个传入函数的参数传入,并将第一个传入函数的返回结果作为值,参数作为键存进对象中缓存。
15.hasChanged 判断是不是有变化
export const hasChanged = (value: any, oldValue: any): boolean =>
!Object.is(value, oldValue)
Object.is是es6新增的方法,
Object.is()
方法判断两个值是否为同一个值。如果满足以下条件则两个值相等:
- 都是
undefined
- 都是
null
- 都是相同长度的字符串且相同字符按相同顺序排列
- 都是相同对象(意味着每个对象有同一个引用)
- 都是数字且
-
- 都是
+0
- 都是
-0
- 都是
-
- 都是
NaN
- 或都是非零而且非
NaN
且为同一个值
- 都是
注意的点:
NaN===NaN =>false
所以,如果要逻辑实现,就需要用 x !== x && y !== y 来判断
16.toNumber 转数字
export const toNumber = (val: any): any => {
const n = parseFloat(val)
return isNaN(n) ? val : n
}
注意的点:isNaN只能判断是不是数,如果参数是字符串,一样会返回true,而Number.isNaN则会判断参数是不是NaN,字符串不是NaN,所以传入字符串是返回false。
总结:
大部分都是学过的基础api,不过也不是经常用,所以老是忘记,趁着这个写笔记机会记录一下。也有部分没见过的,
比如Object.freeze(),冻结对象。还有就是删除数组项的方法,用null代替或者undefined是个不错的选择。判断对象类型用:
Object.prototype.toString.call,可以很方便判断对象的类型,兼容性也还好。NaN的判断也是学到了,因为接触的不多,也不知道NaN===NaN是返回false的。
好记性不如烂笔头,把基础复习了一遍,不错不错~
Σ(っ°Д°;)っ