giscafer / giscafer.github.io

giscafer's blog
http://blog.giscafer.com
10 stars 5 forks source link

lodash源码分析之add方法 #23

Open giscafer opened 6 years ago

giscafer commented 6 years ago

lodash源码分析之add方法

add方法依赖的模块方法有baseGetTag、getTag、isSymbol、baseToNumber、baseToString、createMathOperation

ES6 新增内置对象的Symbol.toStringTag属性值如下。

./.internal/baseGetTag.js

const objectProto = Object.prototype
const hasOwnProperty = objectProto.hasOwnProperty
const toString = objectProto.toString
const symToStringTag = typeof Symbol != 'undefined' ? Symbol.toStringTag : undefined

/**
 * The base implementation of `getTag` without fallbacks for buggy environments.
 * `getTag`方法的基本实现,提供弱环境下的兼容
 * 主要思路:用自带的toString去判断参数value的类型,兼容Symbol类型和自定义类型的情况
 * @private
 * @param {*} value The value to query.
 * @returns {string} Returns the `toStringTag`.
 */
function baseGetTag(value) {
  // null 和 undefined 判断
  if (value == null) {
    return value === undefined ? '[object Undefined]' : '[object Null]'
  }
  // Object(value):将value转为对象,判断value是否为 Symbol 类型属性
  if (!(symToStringTag && symToStringTag in Object(value))) {
    // 如果不是Symbol类型,则 toString.call(value) 返回数据原始类型;
    return toString.call(value);
  }
  // 是否有Symbol.toStringTag属性?
  //  (这里多数情况是false,Symbol.toStringTag类型属性只能是getOwnPropertySymbols来判断或者用Reflect.ownKeys)
  /**
   * 如果value为下列对象情况 isOwn===true;
   * let obj={
        [Symbol.toStringTag]:'toStringTag'
      }
   */
  const isOwn = hasOwnProperty.call(value, symToStringTag);
  /**
   * tag值为三种情况:
   * 1、value为Symbol类型,则tag==="Symbol"
   * 2、value如上边obj情况时,tag===value[symToStringTag]
   * 3、否则为undefined
   */
  const tag = value[symToStringTag]

  // 标记位,并配合try…catch用来检测错误
  let unmasked = false
  try {
    /**
     * 这里试了IE8+的各种浏览器,没能试出报错的情况
     * 主要的作用是去掉obj中[Symbol.toStringTag]作为属性的情况,如注释代码28~30行;
     * toString.call(obj) //"[object toStringTag]"
     */
    value[symToStringTag] = undefined
    unmasked = true
  } catch (e) { }
  //toString.call(obj) //"[object Object]"
  const result = toString.call(value)
  if (unmasked) {//value[symToStringTag] = undefined 没报错的时候
    if (isOwn) {
      value[symToStringTag] = tag
    } else {
      // 去掉强制添加的
      delete value[symToStringTag]
    }
  }
  return result
}

export default baseGetTag

./.internal/getTag.js

主要是功能还是由baseGetTag实现判断,只是该模块对一些对象做了特殊判断处理,修补特殊环境的问题


import baseGetTag from './baseGetTag.js'
// 定义的数据类型
/** `Object#toString` 结果的参照. */
const dataViewTag = '[object DataView]'
const mapTag = '[object Map]'
const objectTag = '[object Object]'
const promiseTag = '[object Promise]'
const setTag = '[object Set]'
const weakMapTag = '[object WeakMap]'

/**
 * 用于检测参数对象是否为 maps, sets, 和 weakmaps 、Promise对象或接口的子类. 
 */
const dataViewCtorString = `${DataView}`
const mapCtorString = `${Map}`
const promiseCtorString = `${Promise}`
const setCtorString = `${Set}`
const weakMapCtorString = `${WeakMap}`

/**
 * Gets the `toStringTag` of `value`.
 * 获取`value`参数的`toStringTag`
 * @private
 * @param {*} value The value to query.
 * @returns {string} Returns the `toStringTag`.
 */
let getTag = baseGetTag;

// Fallback for data views, maps, sets, and weak maps in IE 11 and promises in Node.js < 6.
// 修补特殊环境下的表现不一致的问题,判断getTag()的数据类型是否等于定义的数据类型
// 比如用户环境自己重写覆盖了这些对象或者接口造成不一致的情况
if ((DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag) ||
  (getTag(new Map) != mapTag) ||
  (getTag(Promise.resolve()) != promiseTag) ||
  (getTag(new Set) != setTag) ||
  (getTag(new WeakMap) != weakMapTag)) {
  // 如果表现不一致,兼容处理
  getTag = (value) => {
    // 获取参数的类型检测
    const result = baseGetTag(value)
    // 如果为'[object, Object]' ,返回此对象的构造函数引用,反之undefined
    const Ctor = result == objectTag ? value.constructor : undefined
    // 如果 Ctor 为 true 返回 `${Ctor}`字符串 反之为空
    const ctorString = Ctor ? `${Ctor}` : ''

    if (ctorString) {
      // 比较构造方法字符串是否匹配
      switch (ctorString) {
        case dataViewCtorString: return dataViewTag
        case mapCtorString: return mapTag
        case promiseCtorString: return promiseTag
        case setCtorString: return setTag
        case weakMapCtorString: return weakMapTag
      }
    }
    return result
  }
}

export default getTag

./isSymbol.js

isSymbol 方法用来判断是否为Symbol对象


import getTag from './.internal/getTag.js'

/**
 * Checks if `value` is classified as a `Symbol` primitive or object.
 * 检测`value`值是否为symbol或被定义的
 *
 * @since 4.0.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is a symbol, else `false`.
 * @example
 *
 * isSymbol(Symbol.iterator)
 * // => true
 *
 * isSymbol('abc')
 * // => false
 */
function isSymbol(value) {
  const type = typeof value
  return type == 'symbol' || (type == 'object' && value != null && getTag(value) == '[object Symbol]')
}

export default isSymbol

./.internal/baseToNumber.js

import isSymbol from '../isSymbol.js'

/** 用作各种“Number”常量的参考。 */
const NAN = 0 / 0

/**
 * The base implementation of `toNumber` which doesn't ensure correct
 * conversions of binary, hexadecimal, or octal string values.
 * “toNumber”的基本实现,用来保证二进制、十六进制或八进制字符串值的正确转换。
 *
 * @private
 * @param {*} value The value to process.
 * @returns {number} Returns the number.
 */
function baseToNumber(value) {
  if (typeof value == 'number') {
    return value
  }
  if (isSymbol(value)) {
    return NAN
  }
  return +value
}

export default baseToNumber

./.internal/baseToString.js

import isSymbol from '../isSymbol.js'

/** Used as references for various `Number` constants. */
const INFINITY = 1 / 0

/** 用来将symbol转为原始类型(primitives)和字符串 */
const symbolProto = Symbol ? Symbol.prototype : undefined
const symbolToString = symbolProto ? symbolProto.toString : undefined

/**
 * The base implementation of `toString` which doesn't convert nullish
 * values to empty strings.
 * 
 * `toString`方法的基本实现,保证将空值转换为空字符串。
 *
 * @private
 * @param {*} value The value to process.
 * @returns {string} Returns the string.
 */
function baseToString(value) {
  // 字符串类型,尽早退出以避免某些环境中的性能下降。
  if (typeof value == 'string') {
    return value
  }
  if (Array.isArray(value)) {
    // 递归转换值(易受调用堆栈限制)
    return `${value.map(baseToString)}`
  }
  // 将Symbol类型转为字符串
  if (isSymbol(value)) {
    return symbolToString ? symbolToString.call(value) : ''
  }
  const result = `${value}`;
  // 因为存在将-0转字符串成'0'的可能,所以这里判断保证正确的'-0'
  return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result
}

export default baseToString

./.internal/createMathOperation.js

import baseToNumber from './baseToNumber.js'
import baseToString from './baseToString.js'

/**
 * Creates a function that performs a mathematical operation on two values.
 * 创建一个对两个值执行数学运算的函数
 * @private
 * @param {Function} operator 执行操作的函数
 * @param {number} [defaultValue] undefined 参数默认值
 * @returns {Function} 返回一个新的数学运算函数
 */
function createMathOperation(operator, defaultValue) {
  return (value, other) => {
    if (value === undefined && other === undefined) {
      return defaultValue
    }
    if (value !== undefined && other === undefined) {
      return value
    }
    if (other !== undefined && value === undefined) {
      return other
    }
    if (typeof value == 'string' || typeof other == 'string') {
      value = baseToString(value)
      other = baseToString(other)
    }
    else {
      value = baseToNumber(value)
      other = baseToNumber(other)
    }
    return operator(value, other)
  }
}

export default createMathOperation

最终到_.add方法了,该方法里调用createMathOperation函数创建一个加法回调,所有上边中间的模块都是对方法参数的类型判断,兼容各种类型情况,使得程序按照js数学运算计算出结果并无异常。

import createMathOperation from './.internal/createMathOperation.js'

/**
 * Adds two numbers.
 *
 * @since 3.4.0
 * @category Math
 * @param {number} augend The first number in an addition.
 * @param {number} addend The second number in an addition.
 * @returns {number} Returns the total.
 * @example
 *
 * add(6, 4)
 * // => 10
 */
const add = createMathOperation((augend, addend) => augend + addend, 0)

export default add

简单展示几种类型测试结果 add.test.js


import add from '../add.js';

let obj = {
    [Symbol.toPrimitive](hint) {
        switch (hint) {
            case 'number':
                return 123;
            case 'string':
                return 'str';
            case 'default':
                return 'default';
            default:
                throw new Error();
        }
    }
};

// 一个Symbol.toPrimitive修改过原始类型值得obj对象
console.log(add(obj, 1)); //124

let s = Symbol.for('s');
let ss = Symbol.for('ss');
// 2个Symbol对象
console.log(add(s, ss)); // NaN

let num1 = '1';
let num2 = '2';
// 2个Symbol对象
console.log(add(num1, num2)); //12

let num3 = 1;
let num4 = 2;
// 2个Symbol对象
console.log(add(num3, num4));  //3

源码+测试代码见https://github.com/giscafer/lodash-sourcecode-study 前端学堂:felearn.com

gislu commented 5 years ago

Hi, just stop by for studying purpose. Have a small question here:

in createMathOperation.js we got this function:

function createMathOperation(operator, defaultValue) {
return (value, other) => {

I'm very confused about where the variables 'value' and 'other' come from..... I know it consumes the parameters from add function. But how does it work (how the function createMathOperation capture those arguments from add function and then assign them to 'value' and 'other')?

I have been bothered by this whole day and did some research but still couldn't figure out... I don't even know which key word should I search. Could you kindly plz give me some clues or provide me some links? I really appreciate for that....

(use english here just because it is the only language in my computer so I hope u don't mind it)

giscafer commented 5 years ago

@gislu

in js,function can as a argument, and also can be a return value in another function.

here is an arcticle: https://www.sitepoint.com/currying-in-functional-javascript/

suggest you to learning more about js function

gislu commented 5 years ago

Yeah that's exactly what I'm looking for, thx! 😆 🥂