JS 趣味题与意外Bug

249 阅读12分钟

原文链接,带图片:github.com/jarry/quiz/…

JS 趣味题与找Bug

  1. 比较操作时的隐式转换 (function() { let a = '' let b = [] if (a == b) { console.log('yes') } if (a == !b) { console.log('no') } })() 解释: 隐式类型转换,当数组为空时,取反为false,与空字符串相等,应该使用严格相等比较。 [] == ![] [] == false [] == 0 '' == 0 0 == 0 演示结果: 隐式转换

  2. null不可追加属性 !function() { function setName(obj) { if (typeof obj === 'object') { obj.name = 'abc' } } let obj1 = {} setName(obj1) console.log('obj1:', obj1) let obj2 = null setName(obj2) console.log('obj2:', obj2) }() 解释: null是一个特殊对象,不可再追加属性。 演示结果: 隐式转换

  3. document下的方法不能脱离document运行环境 (function () { const query = document.querySelector let ele = query('div') console.log(ele) })() 解释: querySelector必须由document对象调用,函数内部实现的时候通过this访问了document的相关属性,因此不能脱离document getElementById,createElement方法都类似。 演示结果: 隐式转换

  4. var与let作用域 (function() { var a = 1 if (true) { a = 2 let a } console.log(a) })() 解释: 已存在外部var声明的变量a,花括号内又用let声明了同名变量,此次块级作用域被let 声明的a替代,而在let之前引用变量会报错。如果将let a改为var a 不会报错,或者将a = 2放到let a之后也可以。 演示结果: 隐式转换

  5. 函数默认值声明采用块级作用域 function foo(a = b, b = 2) { console.log(a, b) } foo() 解释: 函数默认值也是块级作用域,此时b在形参a之后,故此报错。 演示结果: 隐式转换

  6. var变量提升 (function() { let a = 1 function foo() { if (a == 1) { console.log('a = 1') } else { var a = 2 console.log('a = 2') } } foo() })() 解释: 由于var声明变量存在提升的问题,因此当let定义外部变量a是,foo内部是可以通过作用域链获取到a的值1, 但是因为在函数内部又用var声明了变量a,因此a在foo函数内部的值一开始是undefined,因此无法进入条件分支a == 1。 演示结果: 隐式转换

  7. function预处理 let a = 1 function foo() { var a if (a == undefined) { console.log('undefined') } else if (a == 1) { console.log(1) } else { console.log('error') } function a() {} } foo() 解释: 函数预处理,外部变量a=1,但内部有定义式函数a,内部a优先,且a的值是函数。 演示结果: 隐式转换

  8. arguments与形参默认值问题 (function() { function foo(a = undefined, b) { arguments[0] = 'a'; arguments[1] = 'b'; if (a == 'a') { console.log('a = a') } if (b == 'b') { console.log('b = b') } } foo(1) })() 解释: 有形参a和b,a有默认值,而b无。调用函数传入了1个参数,也就是给a赋值了。此时通过更改arguments[0]无效,b参数非默认值参数,本可以通过arguments[1]更改,但是调用时并没有传值,即使去掉了形参的默认值,通过arguments来更改也无效。因此避免通过arguments与默认值一块使用。 演示结果: 隐式转换

  9. rest参数设置 (function() { function foo([a], ...b) { if (a == 'a') { console.log('a = a') } if (b == 'b') { console.log('b = b') } } foo(['a', 'b']) })() 解释: 两个参数,第一个是扩展符数组,第二是剩余参数,传入时只传入了一个数组,而忽视了后面的参数。 演示结果: 隐式转换

  10. 词法作用域(静态作用域) (function() { let a = 1 function foo() { let a = 2 return { a: a, getA() { return a } } } const f = foo() f.a = 3 if (f.getA() == f.a) { console.log('equals') } console.log(f.getA(), f.a) })() 解释: a在foo首次执行时返回得到2,f.a更改为3,但getA()函数内的a在定义时的作用域里,通过f.a并不能更改,因此f.getA()里面的a仍然是2。 演示结果: 隐式转换

  11. 去重算法忽略了索引下标 const arr = ['a', 'a', 1, 1, 2, 2, 'b', 'b', 2, 1] function unique(arr) { let len = arr.length for (let i = 0; i < len; i++) { for (let j = i + 1; j < len; j++) { if (arr[i] === arr[j]) { arr.splice(j, 1) len-- break } } } return arr } console.log(unique(arr)) 解释: 双层循环去掉数组重复项,自左往右遍历,移除重复项后长度递减,当前下标也需要对应移动。 演示结果: 隐式转换

  12. 参数值传递 (function() { let a = 1; let b = {x: 'x'} function change(a, b) { a = 2 b = {x: 'y'} } change(a, b) if (a == 2) { console.log('a = 2') } if (b.x == 'y') { console.log('b.x = y') } })() 解释: 参数传递是值而不是引用,因此a和b传递的都是值,将值改变后并不会影响原来的变量。 演示结果: 隐式转换

  13. 意外的分号 (function() { const arr = [1, 2, 3] const len = arr.length for (var i = 0; i < len; i++); { arr[i] += 1 } if (arr.length == len) { console.log('ok') } else { console.log(arr[0] == 2) }

console.log(arr)

})() 解释: 在for循环右侧多出一个意外的分号,导致for循环成为单独语句,从而与下面的赋值脱离。i因为采用var赋值,分号后的花括号可以获取到i的值,因此相当于arr[3] += 1,从而为null,若i采用let声明,则会出现变量i未声明的错误。 演示结果: 隐式转换

  1. 分号引发的ASI问题 (function() { const obj = { foo(a, b) { return a + b } } const add = obj['foo'] [1, 2, 3].map((x) => x += 1) })() 解释: 在函数后面没有跟分号断句,后面又增加了新的调用,这时候被认为是同一个语句,从而报错。 JavaScript有着自动分号插入的机制(Automatic Semicolon Insertion),简称ASI。这时候应该加入分号,或者换一种写法。 演示结果: 隐式转换

  2. 超时与var作用域问题 (function() { for (var i = 0; i < 5; i++) { setTimeout( () => { i += 1 if (i == 1) { console.log('i = 1') } console.log(i) }, 0) } })() 解释: setTimeout是一种异步执行的宏任务,在当前块执行完后再执行,再加上var不区分块级作用域,因此在setTimeout块内执行的i+=1操作,i的值是之前for循环完成之后的值。可以通过闭包或let来解决此问题。 演示结果: 隐式转换

  3. 浮点数问题 (function() { var result = 0.1 + 0.2 if (result == 0.3) { console.log('true') } else { console.log('false') } console.log(result) })() 解释: JavaScript中所有数字包括整数和小数都只有一种类型 — Number。它的实现遵循 IEEE 754 标准,使用64位固定长度来表示,也就是标准的 double 双精度浮点数(相关的还有float 32位单精度)。这时候可以通过toFixed或者换成字符串来比较。 演示结果: 隐式转换

  4. 自动分号(ASI)问题 (function() { function foo() { return { a: 1 } } const result = foo() if (typeof result != 'undefined') { console.log('object') } else { console.log('undefined') } 解释: JS中会自动补齐语句缺失的分号,此例中,return后面的花括号有换行,因此插入了一个分号在return之后,返回值就是undefined,而不是下面的object。 演示结果: 隐式转换

  5. 词法作用域(静态作用域) (function() { var a = 1 function test() { if (a == 2) { console.log('ok') } else { console.log('not 2') } } function foo() { var a = 2 test() } foo() })() 解释: 相较于动态作用域,大部分语言都是静态作用域,JS也是这样。变量a = 1是外部作用域,函数test里的a作用域在其定义的函数空间内,在执行时顺着作用域链往上查找会指向外层作用域,得到1。而foo里面的var a属于foo空间,并不能覆盖外层的作用域。如果将foo里var a的var去掉,直接赋值则相当于修改了外层的变量,此时就会得到2。 演示结果: 隐式转换

  6. 闭包与作用域 (function() { var a = 1 var b = a function bar(a) { return function() { a = a - b return a } } function foo(a) { return bar(a) } var foo1 = foo(1) var result1 = foo1(0) var result2 = foo1(0) if (result1 == a) { console.log('one ok') } if (result1 == result2) { console.log('two ok') } console.log(a, result1, result2) })() 解释: JS是静态作用域,变量作用域在定义时决定,闭包中的a是形参由外部传入,b是外部变量 ,foo1在执行时会更改闭包中的变量a。因此,第一次foo(1)时a=1,foo1(0)后a=0,再次foo1(0)后a=-1。 演示结果: 隐式转换

  7. parseInt错误 (function() { const arr = ['010', '2', '10'] const result = arr.map(parseInt) console.log(result) })() 解释: map接受两个参数, 回调函数 callback和回调函数的this,当前callback有两个参数,当前项与索引。parseInt接受两个参数string与radix。radix可选,表示要解析的数字的基数。该值介于 2 ~ 36 之间。如果省略该参数或其值为 0,则数字将以 10 为基础来解析。如果它以“0x” 或 “0X” 开头,将以 16 为基数。如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN。本例子中,map里callback第二个参数缺省为索引项,相当于arr.map((x, i) => parseInt(x, i)),故此出现意外。 演示结果: 隐式转换JS 趣味题与找Bug

  8. 比较操作时的隐式转换 (function() { let a = '' let b = [] if (a == b) { console.log('yes') } if (a == !b) { console.log('no') } })() 解释: 隐式类型转换,当数组为空时,取反为false,与空字符串相等,应该使用严格相等比较。 [] == ![] [] == false [] == 0 '' == 0 0 == 0 演示结果: 隐式转换

  9. null不可追加属性 !function() { function setName(obj) { if (typeof obj === 'object') { obj.name = 'abc' } } let obj1 = {} setName(obj1) console.log('obj1:', obj1) let obj2 = null setName(obj2) console.log('obj2:', obj2) }() 解释: null是一个特殊对象,不可再追加属性。 演示结果: 隐式转换

  10. document下的方法不能脱离document运行环境 (function () { const query = document.querySelector let ele = query('div') console.log(ele) })() 解释: querySelector必须由document对象调用,函数内部实现的时候通过this访问了document的相关属性,因此不能脱离document getElementById,createElement方法都类似。 演示结果: 隐式转换

  11. var与let作用域 (function() { var a = 1 if (true) { a = 2 let a } console.log(a) })() 解释: 已存在外部var声明的变量a,花括号内又用let声明了同名变量,此次块级作用域被let 声明的a替代,而在let之前引用变量会报错。如果将let a改为var a 不会报错,或者将a = 2放到let a之后也可以。 演示结果: 隐式转换

  12. 函数默认值声明采用块级作用域 function foo(a = b, b = 2) { console.log(a, b) } foo() 解释: 函数默认值也是块级作用域,此时b在形参a之后,故此报错。 演示结果: 隐式转换

  13. var变量提升 (function() { let a = 1 function foo() { if (a == 1) { console.log('a = 1') } else { var a = 2 console.log('a = 2') } } foo() })() 解释: 由于var声明变量存在提升的问题,因此当let定义外部变量a是,foo内部是可以通过作用域链获取到a的值1, 但是因为在函数内部又用var声明了变量a,因此a在foo函数内部的值一开始是undefined,因此无法进入条件分支a == 1。 演示结果: 隐式转换

  14. function预处理 let a = 1 function foo() { var a if (a == undefined) { console.log('undefined') } else if (a == 1) { console.log(1) } else { console.log('error') } function a() {} } foo() 解释: 函数预处理,外部变量a=1,但内部有定义式函数a,内部a优先,且a的值是函数。 演示结果: 隐式转换

  15. arguments与形参默认值问题 (function() { function foo(a = undefined, b) { arguments[0] = 'a'; arguments[1] = 'b'; if (a == 'a') { console.log('a = a') } if (b == 'b') { console.log('b = b') } } foo(1) })() 解释: 有形参a和b,a有默认值,而b无。调用函数传入了1个参数,也就是给a赋值了。此时通过更改arguments[0]无效,b参数非默认值参数,本可以通过arguments[1]更改,但是调用时并没有传值,即使去掉了形参的默认值,通过arguments来更改也无效。因此避免通过arguments与默认值一块使用。 演示结果: 隐式转换

  16. rest参数设置 (function() { function foo([a], ...b) { if (a == 'a') { console.log('a = a') } if (b == 'b') { console.log('b = b') } } foo(['a', 'b']) })() 解释: 两个参数,第一个是扩展符数组,第二是剩余参数,传入时只传入了一个数组,而忽视了后面的参数。 演示结果: 隐式转换

  17. 词法作用域(静态作用域) (function() { let a = 1 function foo() { let a = 2 return { a: a, getA() { return a } } } const f = foo() f.a = 3 if (f.getA() == f.a) { console.log('equals') } console.log(f.getA(), f.a) })() 解释: a在foo首次执行时返回得到2,f.a更改为3,但getA()函数内的a在定义时的作用域里,通过f.a并不能更改,因此f.getA()里面的a仍然是2。 演示结果: 隐式转换

  18. 去重算法忽略了索引下标 const arr = ['a', 'a', 1, 1, 2, 2, 'b', 'b', 2, 1] function unique(arr) { let len = arr.length for (let i = 0; i < len; i++) { for (let j = i + 1; j < len; j++) { if (arr[i] === arr[j]) { arr.splice(j, 1) len-- break } } } return arr } console.log(unique(arr)) 解释: 双层循环去掉数组重复项,自左往右遍历,移除重复项后长度递减,当前下标也需要对应移动。 演示结果: 隐式转换

  19. 参数值传递 (function() { let a = 1; let b = {x: 'x'} function change(a, b) { a = 2 b = {x: 'y'} } change(a, b) if (a == 2) { console.log('a = 2') } if (b.x == 'y') { console.log('b.x = y') } })() 解释: 参数传递是值而不是引用,因此a和b传递的都是值,将值改变后并不会影响原来的变量。 演示结果: 隐式转换

  20. 意外的分号 (function() { const arr = [1, 2, 3] const len = arr.length for (var i = 0; i < len; i++); { arr[i] += 1 } if (arr.length == len) { console.log('ok') } else { console.log(arr[0] == 2) }

console.log(arr)

})() 解释: 在for循环右侧多出一个意外的分号,导致for循环成为单独语句,从而与下面的赋值脱离。i因为采用var赋值,分号后的花括号可以获取到i的值,因此相当于arr[3] += 1,从而为null,若i采用let声明,则会出现变量i未声明的错误。 演示结果: 隐式转换

  1. 分号引发的ASI问题 (function() { const obj = { foo(a, b) { return a + b } } const add = obj['foo'] [1, 2, 3].map((x) => x += 1) })() 解释: 在函数后面没有跟分号断句,后面又增加了新的调用,这时候被认为是同一个语句,从而报错。 JavaScript有着自动分号插入的机制(Automatic Semicolon Insertion),简称ASI。这时候应该加入分号,或者换一种写法。 演示结果: 隐式转换

  2. 超时与var作用域问题 (function() { for (var i = 0; i < 5; i++) { setTimeout( () => { i += 1 if (i == 1) { console.log('i = 1') } console.log(i) }, 0) } })() 解释: setTimeout是一种异步执行的宏任务,在当前块执行完后再执行,再加上var不区分块级作用域,因此在setTimeout块内执行的i+=1操作,i的值是之前for循环完成之后的值。可以通过闭包或let来解决此问题。 演示结果: 隐式转换

  3. 浮点数问题 (function() { var result = 0.1 + 0.2 if (result == 0.3) { console.log('true') } else { console.log('false') } console.log(result) })() 解释: JavaScript中所有数字包括整数和小数都只有一种类型 — Number。它的实现遵循 IEEE 754 标准,使用64位固定长度来表示,也就是标准的 double 双精度浮点数(相关的还有float 32位单精度)。这时候可以通过toFixed或者换成字符串来比较。 演示结果: 隐式转换

  4. 自动分号(ASI)问题 (function() { function foo() { return { a: 1 } } const result = foo() if (typeof result != 'undefined') { console.log('object') } else { console.log('undefined') } 解释: JS中会自动补齐语句缺失的分号,此例中,return后面的花括号有换行,因此插入了一个分号在return之后,返回值就是undefined,而不是下面的object。 演示结果: 隐式转换

  5. 词法作用域(静态作用域) (function() { var a = 1 function test() { if (a == 2) { console.log('ok') } else { console.log('not 2') } } function foo() { var a = 2 test() } foo() })() 解释: 相较于动态作用域,大部分语言都是静态作用域,JS也是这样。变量a = 1是外部作用域,函数test里的a作用域在其定义的函数空间内,在执行时顺着作用域链往上查找会指向外层作用域,得到1。而foo里面的var a属于foo空间,并不能覆盖外层的作用域。如果将foo里var a的var去掉,直接赋值则相当于修改了外层的变量,此时就会得到2。 演示结果: 隐式转换

  6. 闭包与作用域 (function() { var a = 1 var b = a function bar(a) { return function() { a = a - b return a } } function foo(a) { return bar(a) } var foo1 = foo(1) var result1 = foo1(0) var result2 = foo1(0) if (result1 == a) { console.log('one ok') } if (result1 == result2) { console.log('two ok') } console.log(a, result1, result2) })() 解释: JS是静态作用域,变量作用域在定义时决定,闭包中的a是形参由外部传入,b是外部变量 ,foo1在执行时会更改闭包中的变量a。因此,第一次foo(1)时a=1,foo1(0)后a=0,再次foo1(0)后a=-1。 演示结果: 隐式转换

  7. parseInt错误 (function() { const arr = ['010', '2', '10'] const result = arr.map(parseInt) console.log(result) })() 解释: map接受两个参数, 回调函数 callback和回调函数的this,当前callback有两个参数,当前项与索引。parseInt接受两个参数string与radix。radix可选,表示要解析的数字的基数。该值介于 2 ~ 36 之间。如果省略该参数或其值为 0,则数字将以 10 为基础来解析。如果它以“0x” 或 “0X” 开头,将以 16 为基数。如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN。本例子中,map里callback第二个参数缺省为索引项,相当于arr.map((x, i) => parseInt(x, i)),故此出现意外。 演示结果: 隐式转换

原文链接,带图片:github.com/jarry/quiz/…