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
的地方,可以考虑用>>>
做一些防护。