JS 奇技淫巧(二)

正则表达式实例

var a = /123/
var b = /123/

console.log(a == b) // false
console.log(a === b) // false
js

每个字面的正则表达式都是一个单独的实例,即使它们的内容相同。

数组比较大小

var a = [1, 2, 3]
var b = [1, 2, 3]
var c = [1, 2, 4]

console.log(a == b) // false
console.log(a === b) // false
console.log(a > c) // false
console.log(a < c) // true
js

数组也是对象,ES5 规范指出如果两个对象进行相等比较,只有在它们指向同一个对象的情况下才会返回 true,其他情况都返回 false。对象比较大小,会调用 toString 方法转成字符串进行比较,所以结果就变成字符串 “1,2,3" 和 ”1,2,4" 按照字典排序进行比较。

原型把戏

var a = {}
var b = Object.prototype

console.log(a.prototype === b) // false
console.log(Object.getPrototypeOf(a) == b) // true
console.log(a.__proto__ === b) // true
js

对象是没有 prototype 属性的,所以 a.prototypeundefined 。但我们可以通过 Object.getPrototypeOf() 获取 __proto__ 来获取一个对象的原型。

构造函数的函数

function f() { }

var a = f.prototype
var b = Object.getPrototypeOf(f)

console.log(a === b) // false
js

结果是 false。Object.getPrototypeOf 用来获取指定对象的原型(内部 [[Prototype]] 属性的值)。

function Person() { }

var p = new Person()

var a = p.__proto__
var b = Object.getPrototypeOf(p)
var c = Person.prototype

console.log(a === b, a === c, b === c) // true true true

var d = Person.__proto__
var e = Object.getPrototypeOf(Person)
var f = Function.prototype

console.log(d === e, d === f, e === f) // true true true
js

禁止修改函数名

function foo() { }

var oldName = foo.name

foo.name = 'bar'

console.log(oldName, foo.name) // foo foo
js

函数名是只读的,文档 写的很清楚,所以这里的修改无效。

替换陷阱

"1 2 3".replace(/\d/g, parseInt) // 1 NaN 3
js

如果 replace 方法第二个参数是一个函数,则会在匹配的时候多次调用,第一个参数是匹配的字符串,第二个参数是匹配字符串的下标。所以运行时代码实际是 parseInt(1, 0)parseInt(2, 2)parseInt(3, 4)

Function 的名字

function f() { }

var parent = Object.getPrototypeOf(f)

console.log(f.name) // f
console.log(parent.name) // ''
console.log(typeof eval(f.name)) // function
console.log(typeof eval(parent.name)) // error
js

parent 实际上是 Function.prototype,即 ƒ () { [native code] }。它的 name 是 ”。所以 eval(parent.name)undefined

正则测试陷阱

var lowerCaseOnly = /^[a-z]+$/
console.log(lowerCaseOnly.test(null), lowerCaseOnly.test()) // true true
js

test 方法的参数如果不是字符串,会经过抽象 ToString 操作强制转成字符串,因此实际上测试的是字符串 ”null“ 和 ”undefined“。

逗号定义数组

console.log([,,,].join(',')) // ,,
js

JavaScript 允许使用逗号来定义数组,得到的数组是含有 3 个 undefined 值的数组。

所有的数组元素被转成字符串,再用一个分隔符将这些字符串连接起来。如果元素是 undefined 或者 null,则会转换为空字符串。

保留字 class

// chrome
var a = { class: 'Animal', name: "heora" }
console.log(a.class) // Animal
js

这个实际的答案取决于浏览器。class 是关键字,但是在 Chrome、Firefox 和 Opera 中可以作为属性名称,在 IE 中是禁止的。另一方面,其实所有浏览器基本接受大部分的关键字(如:int、private、throws 等)作为变量名,而 class 是禁止的。

无效日期

var a = new Date("epoch")
console.log(a) // Invalid Date
js

实际结果是 Invalid Date ,它实际上是一个 Date 对象,因为 a instance Date 的结果是 true,但是它是无效的 Date。Date 对象内部是用一个数字来存储时间的,在这个例子中,这个数字是 NaN

神鬼莫测的函数长度

var a = Function.length
var b = new Function().length
console.log(a) // 1
console.log(b) // 0
console.log(a === b) // false
js

实际上 a 的值是 1,b 的值是 0 。我们来看下 MDN 文档关于 Function.length 的描述。

Function 构造器本身是一个 Function,它的 length 属性是 1。

Function 原型对象的属性:

Function 原型对象的 length 属性值为 0

在本例中,a 代表的是 Function 构造器的 length 属性,b 代表的是 Function 原型的 length 属性。

Date 的面具

var a = Date(0)
var b = new Date(0)
var c = new Date()

console.log([a === b, b === c, a === c]) // [ false, false, false ]
js

直接看下 MDN 关于对象的注意点:

只能通过调用 Date 构造函数来实例化日期对象:以常规函数调用它(不加 new 操作符)将会返回一个字符串,而不是一个日期对象。另外,不像其他 JavaScript 类型,Date 对象没有字面量格式。

所以 a 是字符串,b 和 c 是 Date 对象,并 b 代表的是 1970 年那个初始化时间,而 c 代表当前时间。

min 和 max 共舞

var min = Math.min()
var max = Math.max()

console.log(min < max) // false
js

对于 Math.min ,如果没有参数,结果为 Infinity。对于 Math.max ,如果没有参数,结果为 -Infinity

最熟悉的陌生人

var a = new Date('2022-06-08')
var b = new Date(2022, 06, 08)

console.log([a.getDay() === b.getDay(), a.getMonth() === b.getMonth()]) // false false
js
new Date(year, monthIndex [, day [, hours [, minutes [, seconds [, milliseconds]]]]]);

当 Date 作为构造函数调用并传入多个参数时,如果数值大于合理范围时(如月份为 13 或者分钟数为70),相邻的数值会被调整。比如 new Date(2013, 13, 1) 等于 new Date(2014, 1, 1),它们都表示日期 2014-02-01(注意月份是从0开始的)。其他数值也是类似,new Date(2013, 2, 1, 0, 70) 等于 new Date(2013, 2, 1, 1, 10),都表示时间 2013-03-01T01:10:00

getDay 返回指定日期对象的星期中的第几天(0~6)。

匹配隐式转换

if ("http://giftwrapped.com/picture.jpg".match(".gif")) {
  console.log("a gif file");
} else {
  console.log("not a gif file");
}
js

如果传入一个非正则表达式对象,则会隐式地使用 new RegExp(obj) 将其转换为正则表达式对象。

所以我们的字符串 ".gif" 会被转换成正则对象 /.gif/,会匹配到 "/gif"

重复声明变量

function foo(a) {
  var a
  return a
}
function bar(a) {
  var a = 'bye'
  return a
}
console.log([foo('hello'), bar('hello')]) // [ 'hello', 'bye' ]
js

一个变量在同一作用域已经声明过,会自动移除 var 声明,但是赋值操作依旧保留。

其实可以参考以下规则解题:

  • 寻找函数的形参和变量声明(变量声明提升)
  • 实参的参数值赋值给形参
  • 寻找函数的函数声明和赋值函数体
  • 执行函数

警惕全局匹配

function captureOne(re, str) {
  var match = re.exec(str)
  return match && match[1]
}

var numRe = /num=(\d+)/ig;
var wordRe = /word=(\w+)/i;

var a1 = captureOne(numRe, "num=1")
var a2 = captureOne(wordRe, "word=1")
var a3 = captureOne(numRe, "NUM=1")
var a4 = captureOne(wordRe, "WORD=1")

console.log([a1 === a2, a3 == a4]) // true false
js

当正则表达式使用 ”g" 标志时,可以多次执行 exec 方法来查找同一个字符串的成功匹配。当你这样做时,查找将从正则表达式的 lastIndex 属性指定的位置开始。所以 a3 的值是 null。

console.log(a1) // 1
console.log(a2) // 1
console.log(a3) // null
console.log(a4) // 1
js