跳至主要內容

Vue3 工具函数源码解析

石怜安大约 18 分钟VUEVUE3VUE

Tips

具体文件是 shared.cjs.prod.jsshared.cjs.js 文件下针对源码中的工具函数和较冷门知识点的提取分析。

vue 版本为 3.2.31

常量定义

EMPTY_OBJ、EMPTY_ARR 对象

typescript是通过 readonly 的方式来定义一个冻结对象的类型的, readonly 是只读修饰符。 Object.freeze 冻结对象属性功能。

  • 源码实现
export declare const EMPTY_OBJ: {
    readonly [key: string]: any;
} = __DEV__ ? Object.freeze({}) : {}


export declare const EMPTY_ARR: {
    readonly [key: string]: any;
} = __DEV__ ? Object.freeze([]) : []
  • 使用案例
let emptyObj = Object.freeze({
  props: {
    name: 'jack',
    age: 12
  },
  total: 1000
})
// 无法修改第一层属性
emptyObj.total = 0
console.log('emptyObj.total', emptyObj.total)
// 可以修改第二层对象的属性
emptyObj.props.age = 13
console.log('emptyObj.props.age', emptyObj.props.age)
// 无法新建本不存在的属性
emptyObj.props2 = {}
console.log('emptyObj.props2', emptyObj.props2)


let arr = Object.freeze([])
let arr1 = Object.freeze([{ name: 'jack' }])
// arr.push(1) // 无法添加元素, 会直接报错
arr.length = 3
console.log('arr', arr)
// 对象内的属性可以修改
arr1[0].name = 'rose'
console.log('arr1[0].name', arr1[0].name) // rose

NOOP 空函数

定义一个空函数,而不是 function () {} 这样定义,是为了方便压缩。

  • 源码实现
export const NOOP = () => {}
  • 使用案例
let obj = function getName (cb => NOOP) {}

NO 返回false常量函数

永远返回 false 的函数, 就是一个返回 boolean 值的函数的备选项

  • 源码实现
export const NO = () => false

函数定义

toTypeString 复杂数据类型区分

toTypeString 主要是返回数据的类型,使用了 Object.prototype.toString.call 的方法,实现了对复杂数据类型的区分

  • 源码实现
const objectToString = Object.prototype.toString;
const toTypeString = (value) => objectToString.call(value);
  • 使用案例
let arr = []
let obj = {}
let map = new Map()
let set = new Set()
console.log(toTypeString(obj)) // [object Object]
console.log(toTypeString(arr)) // [object Array]
console.log(toTypeString(map)) // [object Map]
console.log(toTypeString(set)) // [object Set]

toRawType 取出数据类型

这里需要注意的是前面的 toTypeString 返回了 [object xxType],现在则是使用 slice 方法来将 xxType 取出

  • 源码实现
export const toRawType = (value: unknown): string => {
  // extract "RawType" from strings like "[object RawType]"
  return toTypeString(value).slice(8, -1)
}
  • 使用案例
const objectToString = Object.prototype.toString
const toTypeString = (value) => objectToString.call(value)

const toRawType = (value) => toTypeString(value).slice(8, -1)

const str = toRawType('')
console.log('str', str) // 'String'
const num = toRawType(123)
console.log('num', num) // 'Number'

isOn 事件名on判断

利用正则来判断当前的事件名是否是 on + EventName 的格式

注意: ^ 在正则开头表示首位占位符,其他地方都是非的含义,[^a-z] 表示不是 a 到 z 的字母

  • 源码实现
const onRE = /^on[^a-z]/
export const isOn = (key: string) => onRE.test(key)

isModelListener 事件名onUpdate判断

检验监听事件名是否是onUpdate:开头

  • 源码实现
export const isModelListener = (key: string) => key.startsWith('onUpdate:')

startsWithes6 当中的方法,可以获取一个字符串是否以指定的子字符串开头,返回Boolean类型

  • 使用案例
console.log(isModeListener('onUpdate:change'))

extend 合并对象

  • 源码实现
const extend = Object.assign
  • 使用案例
let obj1 = {name: 'jack'}
let obj2 = {name: 'rose', age: 18}

let obj = extend(obj1, obj2)
console.log('obj', obj) // { name: 'rose', age: 18 }
// 注意, 原本的obj1也会被改变
console.log('obj1', obj1) // { name: 'rose', age: 18 }

remove 删除数组元素

删除数组中的某个元素,但是使用splice方法,其实是比较消耗性能的。

  • 源码实现
export const remove = <T>(arr: T[], el: T) => {
  const i = arr.indexOf(el)
  if (i > -1) {
    arr.splice(i, 1)
  }
}
  • 使用案例
let arr = [1, 2, 3]
remove(arr, 2) // [ 1, 3 ] 'arr数据'
console.log(arr, 'arr数据')

Tips

axios源码中 lib/core/interceptorManager.js , 使用以下的方式删除数组中的元素:

InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};

// 第46行
/**
 * Iterate over all the registered interceptors
 *
 * This method is particularly useful for skipping over any
 * interceptors that may have become `null` calling `eject`.
 *
 * @param {Function} fn The function to call for each interceptor
 */
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};

hasOwn 是否包含属性

判断一个属性是否是一个对象本身的属性,利用了 Object.prototype.hasOwnProperty.call 的形式来实现功能

  • 源码实现
const hasOwnProperty = Object.prototype.hasOwnProperty

/**
 * Vue3 的写法
 */
export const hasOwn = (
  val: object,
  key: string | symbol
): key is keyof typeof val => hasOwnProperty.call(val, key)

/**
 * Vue3.2 的写法
 */
export const hasOwn = (
  val: object,
  key: string | symbol
): key is never => hasOwnProperty.call(val, key)

Tips

  • is 关键字:它被称为类型谓词,用来判断一个变量属于某个接口或类型,比如:
const isNumber = (val: unknown): val is number => typeof val === 'number'
const isString = (val: unknown): val is string => typeof val === 'string'
  • keyof 关键字:用于获取某种类型的所有键,其返回类型是联合类型,比如:
interface Person {
    name: string;
    age: number;
}
type K = keyof Person; // "name" | "age"
  • typeof 关键字:js 中的 typeof 只能获取几种类型,而在 ts 中 typeof 用来获取一个变量声明或对象的类型,比如:
interface Person {
  name: string;
  age: number;
}
 
const sem: Person = { name: 'semlinker', age: 30 };
type Sem = typeof sem; // -> Person

isArray 是否数组

  • 源码实现
export const isArray = (arg: any) : arg is any[] => Array.isArray(arg);
  • 使用案例
const fakeArray = { __proto__: Array.prototype, length: 0 }

console.log('isArray(fakeArray)', isArray(fakeArray)) // false
console.log('fakeArray instanceof Array', fakeArray instanceof Array) // true

isMap/isSet 是否Map/Set

  • 源码实现
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]'

Map

Map 是一种 es6 提供的新的一种键值对数据结构的数据类型,相比于对象,它的键不同于对象那种只能是字符串的键,可以是各种类型。

  • 基础方法

get 获取元素, set 新增元素成员, has 是否包含某元素, delete 删除元素成员, clear 清空所有元素, Array.from 转为普通的二维数组

// 1. 定义一个函数作为键
let fn = function func() { console.log('this is function') }
let m = new Map([['jack', 100], [fn, '我是函数的值']]) // 形式上, Map类型是二维数组

// 2. get方法获取元素
let result = m.get(fn)
console.log('func -> result', result)
// func -> result 我是函数的值

// 3. 通过Array.from可以转为普通的二维数组
let arr = Array.from(m)
console.log('func -> arr', arr)
// func -> arr [ [ 'jack', 100 ], [ [Function: func], '我是函数的值' ] ]

// 4. set方法新增元素成员
let obj = { name: 'jack' }
m.set(obj, '28岁了都')
console.log('set新元素之后', m)
// set新元素之后 Map {
// 'jack' => 100,
//  [Function: func] => '我是函数的值',
//  { name: 'jack' } => '28岁了都' }

// 5. has 判断是否包含某元素
m.has(fn)
console.log('func -> m.has(fn)', m.has(fn)) // true

// 6. delete删除元素成员
m.delete(obj)
console.log('删除后的结果', Array.from(m))
// 删除后的结果 [ [ 'jack', 100 ], [ [Function: func], '我是函数的值' ] ]

// 7. clear清空所有元素
m.clear()
console.log('清空后的结果', m)
  • 遍历相关的方法

keys 返回包含映射中 的迭代器对象,entries 返回包含映射中的 键值 的迭代器对象,values 返回包含映射中的 的迭代器对象,forEach

let fn = function func() { console.log('this is function') }
let m = new Map([['jack', 100], [fn, '我是函数的值']])
m.forEach(item => {
  console.log('forEach -> item', item)
})
/**
 * forEach -> item 100
 * forEach -> item 我是函数的值
 */


// 1. keys 方法, 返回包含映射中键的迭代器对象
let it = m.keys()
console.log('it', it) // it [Map Iterator] { 'jack', [Function: func] }
console.log(it.next().value) // jack
console.log(it.next().value) // [Function: func]
console.log(it.next().value) // undefined
// 或者
for (let key of it) {
  console.log('for...of... -> key', key)
}
/**
 * 
  for...of... -> key jack
  for...of... -> key function func() {
    console.log('this is function');
  }
 */

// 2. entries 方法, 返回包含映射中的键值的迭代器对象
let it = m.entries()
console.log(it.next().value) // [ 'jack', 100 ]
console.log(it.next().value) // [ [Function: func], '我是函数的值' ]
console.log(it.next().value) // undefined
// 或者
for (let item of it) {
  console.log('for...of... -> entries', item)
}
/**
 * for...of... -> entries [ 'jack', 100 ]
 * for...of... -> entries [ [Function: func], '我是函数的值' ]
 */

// 3. values 方法, 返回包含映射中的值的迭代器对象
let it = m.values()
console.log(it.next().value) // 100
console.log(it.next().value) // 我是函数的值
console.log(it.next().value) // undefined
// 或者
for (let value of it) {
	console.log('for...of... -> value', value)
}
// /**
//  * for...of... -> value 100
//  * for...of... -> value 我是函数的值
//  */

::: theorem Map和Object的区别:

  • Map的键可以是任意类型, Object只能是 String 或者 Symbol
  • Map的可以通过size属性获取元素数量, Object则必须手动计算
  • Map在频繁增减键值对的场景下, 性能较好
  • Map中的数据是有序的, 而Object则是无序的 :::

Set

Set类型是es6提供的一种新的数据类型,它允许你存入 任意类型唯一值 ,无论是 基本数据类型 还是 引用类型 ,尽管NaN !== NaN,Set仍然认为这是同一个数据。

  • 基础方法

add 新增元素成员, has 是否包含某元素, delete 删除元素成员, clear 清空所有元素, Array.from 转为数组

// 1. NaN
let set = new Set([NaN, NaN])
// 尽管NaN !== NaN, 但是, 在Set中仍然被认为是相同的数据
console.log('NaN === NaN', NaN === NaN) // false
console.log('set', set) // Set {NaN}

// 2.add 方法
let set = new Set()
let person1 = {
  name: '大明'
}
let person2 = {
  name: '小明'
}
set.add(person1)
set.add(person2)
console.log('add的结果', set)
// add的结果 Set { { name: '大明' }, { name: '小明' } }

console.log('Array.from', Array.from(set))
// Array.from [ { name: '大明' }, { name: '小明' } ]

// 3. delete方法
set.delete(person1)
console.log('delete之后', set) // delete之后 Set { { name: '小明' } }

// 4. has方法
console.log('has person1 -->', set.has(person1))
// has person1 --> false
console.log('has person2 -->', set.has(person2))
// has person2 --> true

// 5.清空
set.clear()
console.log('set clear -->', set) // set clear --> Set {}
  • 遍历相关的方法

主要有keysentriesvaluesforEach

// 1. keys方法
let it = set.keys()
console.log(it.next().value) // { name: '大明' }
console.log(it.next().value) // { name: '小明' }
console.log(it.next().value) // undefined
// 或者
for (let key of it) {
  console.log('for...of... -> keys', key)
}
/**
 * for...of... -> keys { name: '大明' }
 * for...of... -> keys { name: '小明' }
 */

// 2. entries 方法
let it = set.entries() // SetIterator {{…} => {…}, {…} => {…}}
for (let key of it) {
  console.log('for...of... -> entries', key)
}
/**
 * for...of... -> entries [ { name: '大明' }, { name: '大明' } ]
 * for...of... -> entries [ { name: '小明' }, { name: '小明' } ]
 */

// 3. values
let it = set.values() // SetIterator {{…}, {…}}
console.log(it.next().value) // { name: '大明' }
console.log(it.next().value) // { name: '小明' }
console.log(it.next().value) // undefined
// 或者
for (let key of it) {
  console.log('for...of... -> values', key)
}
/**
 * for...of... -> values { name: '大明' }
 * for...of... -> values { name: '小明' }
 */

// 4. forEach
set.forEach(item => {
  console.log('item', item)
})
/**
 * item { name: '大明' }
 * item { name: '小明' }
 */

isDate 是否时间

  • 源码实现 有风险
export const isDate = (val: unknown): val is Date => val instanceof Date

这么做有一定的漏洞,但一般还是可以判断

const isDate = (val) => val instanceof Date
let date = new Date()
let result = isDate({ __proto__: Date.prototype, length: 0 })
console.log('result', result) // result true

isFunction 是否函数

  • 源码实现
export const isFunction = (val: unknown): val is Function => typeof val === 'function'

isObject 是否对象

  • 注意事项

typeof null === 'object',所以必须确保val不为null。

Recordtypescript 中的一种工具类型,它的作用是限制一个对象的键值类型,其两个泛型参数就是一个限制键类型,一个限制值类型。

  • 源码实现
export const isObject = (val: unknown): val is Record<any, any> => val !== null && typeof val === 'object'

Tips

// 第1469行代码
type Record<K extends keyof any, T> = {
    [P in K]: T;
};
// 从这里我们可以看出, 键的类型只能从K(第一个泛型参数中得到), 而值的类型只能是T

Record使用案例:

type Animal = 'dog' | 'cat' | 'pig'

interface Info {
  name: string;
  age: number
}

type AnimalInfo = Record<Animal, Info>

// animalInfo的键, 只能是dog, cat, pig中的一个
const animalInfo: AnimalInfo = {
  dog: {
    name: '阿旺',
    age: 2
  },
  cat: {
    name: '阿花',
    age: 1
  },
  pig: {
    name: '二师兄',
    age: 3
  }
}

isPlainObject 是否纯粹对象

该方法作用是,判断一个对象是否是纯粹的对象,前面一个isObject方法,isObject([])是trueisObject({})也是true,而此处的isPlainObject则仅限于真正的Object。

  • 源码实现
export const isPlainObject = (val: unknown): val is object => toTypeString(val) === '[object Object]'

isPromise 是否Promise

判断是否是promise对象,这里要注意的是Promise的类型,typescript 中 Promise<T> 类型,接受一个泛型参数T,用以确定这个promise对象最终resolve的值的类型

  • 源码实现
export const isPromise = <T = any>(val: unknown): val is Promise<T> => isObject(val) && isFunction(val.then) && isFunction(val.catch)

Tips

我们如何控制声明Promise返回值的类型:

  • 使用这里的泛型方式声明
let promiseString:Promise<string> = new Promise(resolve => resolve('123'))
let promiseNumber:Promise<number> = new Promise(resolve => resolve('123'))
  • 单独声明resolve方法
let promiseString = new Promise((resolve: (params: string) => void, reject) => resolve('123'))

isIntegerKey 是否数字型的字符串

主要是用于判断是否是数字型的字符串,形如: '123','888' 则为true,'123hello'则为false。

  • 源码实现
export const isString = (val: unknown): val is string => typeof val === 'string'
export const isIntegerKey = (key: unknown) =>
  isString(key) &&
  key !== 'NaN' &&
  key[0] !== '-' &&
  '' + parseInt(key, 10) === key
isIntegerKey('888hello') // false

Tips

parseInt 的第一个参数大家都很熟悉,就是要被转换的字符串,但是第二个出现的概率可能相对偏低,第二个表示的就是进制,一般默认是10,也就是十进制! 这里指明进制数是为了保证在不同的环境下运行结果能保证一致!

// 以二进制的方式解析'010'
const result = parseInt('010', 2)
console.log('result', result) // 2
// 我们都知道,如果 '010'是二进制,那么,转为10进制,就是使用: 0*2^0 + 1*2^1 + 0*2^2 结果自然就是2,同理我们可以知道用三进制来解析:
const result = parseInt('010', 3) // 结果自然就是3
思考

第二个参数的取值最大能达到多少?

猜测

我们知道,十进制最大的数也就是9,那么如果我要解析十进制以上的数字呢? 最常见的就是十六进制。不错,我们会用字母代替!也就是a-z,共26个字母,那么我们大胆猜测下,最大取值,是不是就是36?

const result = parseInt('010', 36)
console.log('result', result) // 36
// 那再往上加一呢:
const result = parseInt('010', 37)
console.log('result', result) // NaN

由此我们得知, parseInt最多只能取到36!

makeMap 构造带逗号的字符串校验字符函数

该方法主要是接收一个带逗号的字符串,将该字符串以逗号拆分为一个个子字符串,再用这些子字符串作为一个对象的值全部为true;返回一个方法,这个方法可以检测出这个方法接收的参数是否是对象中的键。

  • 源码实现
export function makeMap(
  str: string,
  expectsLowerCase?: boolean
): (key: string) => boolean {
  const map: Record<string, boolean> = Object.create(null)
  const list: Array<string> = str.split(',')
  for (let i = 0; i < list.length; i++) {
    map[list[i]] = true
  }
  return expectsLowerCase ? val => !!map[val.toLowerCase()] : val => !!map[val]
}
  • 难点解析

类型上来看,其实就是一个这样的方法:(params1:string, params2?:boolean) => (key: string) => boolean,其返回了一个检测函数,该检测函数接受一个字符串,返回是该字符串是否存在!

  • 使用案例
const fn = makeMap('dog,cat,bird')
const result1 = fn('fish')
console.log(result1) // false, 不存在fish
const result2 = fn('dog')
console.log(result2) // true, 存在dog

Tips

思考

创建对象为何要用 Object.create(null) 而不是直接使用 const map = {}

解答

Object.create(proto, [propertiesObject]),返回一个新的对象。第一个参数proto,将会被挂在到新对象的原型对象上。 第二个参数propertiesObject,对应了Object.defineProperties的第二个参数,也就是所谓的属性描述符:

  • value 属性的值
  • writable 是否可以写,默认为true
  • enumerable 属性是否可枚举, 所谓可枚举,就是能够被以下方法访问到
  • for...in 能将该属性遍历出来
  • Object.keys 能将该属性包含在返回的数组中
  • JSON.stringify 能够将其变为JSON字符串的一部分
  • configurable 默认true,如果为false,则属性无法被改变,无法被删除,无法修改属性描述符
  • set 存值函数
  • get 取值函数

我们可以看到,Object.create(null)创建的对象更为纯粹,当方法执行到map[val.toLowerCase()]时,不会受到__proto__的影响。

cacheStringFunction 缓存结果函数

函数返回一个函数,这个函数接收一个字符串参数,如果第一次传入了一个参数,计算结果就会被闭包``缓存起来,下次再遇到相同参数的时候,就不会再走fn方法重新计算了。 本质上是一个单例模式,利用闭包,保存了之前的计算结果。

  • 源码实现
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
}
  • 使用案例
let fn1 = cacheStringFunction((key) => {
  console.log('通过了计算得到', key + 'world')
  return key + 'world'
})
console.log(fn1('hello'))
console.log(fn1('hello'))
console.log(fn1('goodbye'))
/**
 * 
 * 通过了计算得到 helloworld
 * helloworld
 * helloworld
 * 通过了计算得到 goodbyeworld
 * goodbyeworld
 */

Tips

思考

为何返回的函数要被as any?去掉会如何?

猜测

(str:string) => string 是符合 T 的类型要求,但是,T也可以是另一种形式的子类,也就无法保证和参数的类型完全一致。举个例子,假如以下函数不报错:

let testGenerics = <T extends { length: number }>(params: T, minNum: number): T =>{
  if (params.length >= minNum) {
    return params
  } else {
    return { length: minNum } as T
  }
}

那我们直接运行下

let data = testGenerics([1,2,3], 8)
// 此时的data,讲道理应该是Array类型
data.slice(0,1) // 直接报错, 因为根本就不是数组!

camelize 驼峰转化

  • 源码实现
const camelizeRE = /-(\w)/g
export const camelize = cacheStringFunction((str: string): string => {
  return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''))
})
  • 使用案例
let str = 'on-handle-click'
const result = camelize(str)
console.log('result', result) // result onHandleClick

Tips

关于replace的使用: replace(regexp|substr, newSubStr|function)

第一个参数既可以是字符串,也可以是正则,总之就是需要被替换的字符串的文本模式

第二个参数,它既可以是用于替换掉第一个参数在原字符串中的匹配部分的字符串(该字符串中可以内插一些特殊的变量名),也可以是一个用来创建新子字符串的函数,该函数的返回值将替换掉第一个参数匹配到的结果。

  • 正则替换表达式

$& 用于无分组的情况

let str = '史记真是史家之绝唱,无韵之离骚'
let result = str.replace('史记', '《$&》') // 这里的$&就是`史记`二字, 也就是用《史记》代替史记
console.log(result) // 《史记》真是史家之绝唱,无韵之离骚

$` 匹配到的数据的左边字符串

let str = '研究一下replace该怎么用'
let result = str.replace('replace', ',$`前端技术') // 这里的 $` === 研究一下,也就是用 ',研究一下前端技术' 代替 'replace' 
console.log(result) // 研究一下,研究一下前端技术该怎么用

$' 和 $` 相反,代表匹配到的数据的右边字符串

let str = '研究一下replace该怎么用'
let result = str.replace('replace', ",vue3$',") // 此处的 $' === 该怎么用,也就是用 ',vue3该怎么用,' 代替 'replace'
console.log(result) // 研究一下,vue3该怎么用,该怎么用

$1,$2,$3,.....$n,表示第几个分组

let str = '西瓜,番薯,大番薯,咸鱼,萝卜,苹果'
let result = str.replace(/(西瓜)(.*)(苹果)/, "$1(水果)$2$3(水果)")
/**
 * $1 === 西瓜
 * $2 === ,番薯,大番薯,咸鱼,萝卜,
 * $3 === 苹果
 */
console.log('result', result) // 西瓜(水果),番薯,大番薯,咸鱼,萝卜,苹果(水果)
  • 函数
let str = '今年是2022年,时间好快'
let result = str.replace(/(今年).+?(时间).*/g, function () {
  console.log(arguments)
  /**
   * 0: "今年是2022年,时间好快"
   * 1: "今年"
   * 2: "时间"
   * 3: 0
   * 4: "今年是2022年,时间好快"
   */
})

可以得出结论,那就是有分组的情况下,第二个参数开始就是依次展示每次分组匹配到的内容。 所以,我们回到源码中,此处的c,实际上就是前面说的每次匹配到的第一个分组,本案例中依次为:h, c两个,然后将其改为大写,直接return,就能将-x替换为X,从而实现我们的目标。

let str = 'on-handle-click'
let result = str.replace(/-(\w)/g, function () {
  console.log(arguments)
  // { '0': '-h', '1': 'h', '2': 2, '3': 'on-handle-click' }
  // { '0': '-c', '1': 'c', '2': 9, '3': 'on-handle-click' }
  return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''))
})

hasChanged 比较变量相同

  • 源码实现
// compare whether a value has changed, accounting for NaN.
export const hasChanged = (value: any, oldValue: any): boolean => !Object.is(value, oldValue)

Tips

可能有人感到疑问,两个值是否不同还需要封装?多此一举,我直接 a !== b 不就行了?我们来看几个例子:

// +0 和 -0问题
console.log(+0 === -0) // true
Object.is(+0, -0) // false

// NaN 问题
console.log(NaN === NaN) // false
Object.is(NaN, NaN) // true

由此可以看出,Object.is可以弥补 正负0NaN 比较上存在的问题。MDN网站上还提供了一个polyfill

Object.is = function () {
	 // 如果两个值不同(有可能是正负0)
  if (x === y) {
    return x !== 0 || 1/x === 1/y
    /**
     * 如果x,y分别为+0 和 -0, 那么, 一个将会是Infinity 另一个是-Infinity
     */
    // 如果不同, 也可能是NaN
  } else {
    // 自己都不等于自己, 那肯定是NaN了
    return x !== x && y !==y
  }
}
console.log('NaN === NaN -->', Object.is(NaN, NaN))
console.log('+0 === -0 -->', Object.is(+0, -0))
// NaN === NaN --> true ⠼ : timing npm:load:cleanupLog Completed in 2ms
// +0 === -0 --> false

def 添加不可枚举属性

就是给对象obj,加上一个可以删除,其属性描述符可以改变,且不可枚举的属性key,其值为value

  • 源码实现
export const def = (obj: object, key: string | symbol, value: any) => {
  Object.defineProperty(obj, key, {
    configurable: true,
    enumerable: false,
    value
  })
}
  • 使用案例
let person = {
  name: 'human',
  age: 100
}
def(person, 'gender', 'male')
console.log('person --> ', person) 
/**
 * nodejs 环境下 { name: 'human', age: 100 }
 * chrome 浏览器下 { name: 'human', age: 100, gender: 'male' }
 *  */ 
console.log('gender --> ', person.gender) // male

测试可枚举性,按照我们之前说的for...inObject.keysJSON.stringify三种方法

// for...in
for (let key in person) {
  console.log('key', key)
  /**
   * key name
   * key age
   */
}

// JSON.stringify
console.log('JSON.stringify(person)', JSON.stringify(person))
// {"name":"human","age":100}

// Object.keys(person)
console.log('Object.keys(person)', Object.keys(person))
// [ 'name', 'age' ]

Tips

属性描述符可以细分为数据描述符存取描述符。注意,configurable 和 enumerable既是数据描述符又是存取描述符。除了这两个属性之外,其他不同的描述符不得共用!

数据描述符:writable 只有writable为true的时候,该属性才能被改变值。 value 属性的值

存取描述符: getset

toNumber 尝试转换数字

  • 源码实现
export const toNumber = (val: any): any => {
  const n = parseFloat(val)
  return isNaN(n) ? val : n
}

Tips

isNaN一看字面意思就知道: 判断一个值是否为NaN. 但他有一些怪异行为, 例如:

isNaN(undefined) // true
isNaN('undefined') // true
isNaN('haha') // true

很明显, 这个方法关心的根本不是一个值是否是NaN, 它似乎更关心一个值是否无法被转为数字! 所以, 我们有了Number.isNaN

Number.isNaN(undefined) // false
Number.isNaN('undefined') // false
Number.isNaN('haha') // false

所以, 一定要注意了, isNaNNumber.isNaN不是一回事!