baseGetTag 获取变量类型
以前获取变量类型主要有 3 种方法:
typeof
typeof 主要用来判断变量是否为原生值类型,对于引用类型其均返回Object:
1  | typeof 1; // 'number'  | 
instanceof
instanceof 用来确定左操作数是否在右操作数的原型链上,并且在有多个frame时可能会出问题。具体机制可参见这篇博客。
1  | function T() {}  | 
Object.prototype.toString.call
Object.prototype.toString.call 应当来说这个是推荐用法了。见MDN的描述:
每个对象都有一个 toString()方法,当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时自动调用。默认情况下,toString()方法被每个 Object 对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 “[object type]”,其中 type 是对象的类型。
由于toString方法可能会被对象覆盖,所有要用上述的形式调用,而不是简单的obj.toString.
toString 的具体工作机制如下(参考):
1. 如果 `this` 的值是 `undefined`, 返回 `[object Undefined]`.
- 如果 
this的值是null, 返回[object Null]. - 令 
O为以this作为参数调用ToObject的结果 . - 令 
class为O的[[Class]]内部属性的值 . - 返回三个字符串 
[object,class, 和]连起来的字符串 . 
每个内置对象都定义了[[Class]]内部属性,有"Arguments", "Array", "Boolean", "Date", "Error", "Function", "JSON", "Math", "Number", "Object", "RegExp", "String"
此方法在 ES5 中工作的很好,但在 ES6 中,添加了一种新的Symbol类型,以及一个内置的Symbol值Symbol.toStringTag,它拦截了toString的工作。Symbol.toStringTag应该被定义成一个getter,它的返回值代表变量的类型。
1  | class Normal {}  | 
The Symbol.toStringTag well-known symbol is a string valued property that is used in the creation of the default string description of an object. It is accessed internally by the Object.prototype.toString() method.
MDN上的描述说到Symbol.toStringTag其实在toString内部用到了。 具体流程如下:
- 如果 this 是 undefined ,返回 ‘[object Undefined]’ ;
 - 如果 this 是 null , 返回 ‘[object Null]’ ;
 - 令 O 为以 this 作为参数调用 ToObject 的结果;
 - 令 isArray 为 IsArray(O) ;
 - ReturnIfAbrupt(isArray) (如果 isArray 不是一个正常值,比如抛出一个错误,中断执行);
 - 如果 isArray 为 true , 令 builtinTag 为 ‘Array’ ;
 - else ,如果 O is an exotic String object , 令 builtinTag 为 ‘String’ ;
 - else ,如果 O 含有 [[ParameterMap]] internal slot, , 令 builtinTag 为 ‘Arguments’;
 - else ,如果 O 含有 [[Call]] internal method , 令 builtinTag 为 Function ;
 - else ,如果 O 含有 [[ErrorData]] internal slot , 令 builtinTag 为 Error ;
 - else ,如果 O 含有 [[BooleanData]] internal slot , 令 builtinTag 为 Boolean ;
 - else ,如果 O 含有 [[NumberData]] internal slot , 令 builtinTag 为 Number ;
 - else ,如果 O 含有 [[DateValue]] internal slot , 令 builtinTag 为 Date ;
 - else ,如果 O 含有 [[RegExpMatcher]] internal slot , 令 builtinTag 为 RegExp ;
 - else , 令 builtinTag 为 Object ;
 - 令 tag 为 Get(O, @@toStringTag) 的返回值( Get(O, @@toStringTag) 方法,既是在 O 是一个对象,并且具有 @@toStringTag 属性时,返回 O[Symbol.toStringTag] );
 - ReturnIfAbrupt(tag) ,如果 tag 是正常值,继续执行下一步;
 - 如果 Type(tag) 不是一个字符串,let tag be builtinTag ;
 - 返回由三个字符串 “[object”, tag, and “]” 拼接而成的一个字符串。
 
前 15 步可以看成跟  es5  的作用一样,获取到数据的类型  builtinTag ,但是第 16 步调用了  @@toStringTag  的方法,其实就是Symbol.toStringTag对应的方法。 最终结果优先以这个方法返回值为准,不行的话再使用builtinTag
baseGetTag
lodash中的baseGetTag为我们封装了上述所有逻辑:
1  | const objectProto = Object.prototype;  | 
一段段来看下:
1  | if (value == null) {  | 
这是对入参为空时的判断,没什么好说的。
1  | if (!(symToStringTag && symToStringTag in Object(value))) {  | 
如果环境不支持Symbol.toStringTag或者Symbol.toStringTag没有在对象上没有定义,那么都直接调用原始的toString即可。
1  | const isOwn = hasOwnProperty.call(value, symToStringTag);  | 
isOwn判断symToStringTag是在自身还是原型链上;tag用来备份; 之后try...catch里的value[symToStringTag] = undefined困扰了我很久,不知道什么情况下会报错,后来突然想到Object.defineProperty,里面如果writable为false,或者只指定了get没有set,都会报错:
1  | ;  | 
最后一段
1  | const result = toString.call(value);  | 
如果symToStringTag是对象自身的,那么还原回去。从try...catch到后面的if分支,主要是避免对象自身的symToStringTag对最终结果的影响。如:
1  | var o = {  | 
isFunction
以前判断一个变量是否为函数可以很简单的用typeof就行:
1  | function t() {}  | 
或者使用Object.prototype.toString:
1  | Object.prototype.toString.call(t); // [object Function]  | 
看看Lodash中是怎么实现的:
1  | function isFunction(value) {  | 
挨个做一下测试:
1  | function normalFunc() {}  | 
[object Proxy]的情况没有试出来,ES6 的Proxy不是函数:
1  | var proxy = new Proxy({}, {});  | 
但是在underscore.js中isFunction的实现就是直接利用的typeof:
1  | // Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8,  | 
综上,除了在一些『古董』上,使用typeof来判定函数是完全 ok 的。
>>> 操作符
在lodash中经常会看到这样的代码:
1  | length = start > end ? 0 : (end - start) >>> 0;  | 
这里的>>>是干嘛的?
在 js 中,Array.length需要是一个 0~2^31 -1 之间的无符号整数,参考MDN。而>>>是一个无符号右移运算符,正好可以帮助我们做到这点。
a >>> b的作用是将a的二进制表示向右移b(<32)位,丢弃被移出的位,并使用 0 在左侧填充。于是操作结果就总是一个 0~2^31 -1 之间的无符号整数。搬运 MDN 上的例子:
1  | 9 (base 10): 00000000000000000000000000001001 (base 2)  | 
同时经测试它还能包容一些异常情况:
1  | '1' >>> 0; // 1  | 
有另外一个>>操作符,对于a >> b,它的作用是将 a 的二进制表示向右移  b (< 32) 位,丢弃被移出的位。如果 a 是一个非负数,那么>>和>>>的作用是一样的,差别在于负数,它会在左侧填充 1,而不是 0:
1  | -9 (base 10): 11111111111111111111111111110111 (base 2)  | 
因此如果有用到Array.length的地方,可以考虑用>>>做一些防护。