专栏名称: 程序员大咖
为程序员提供最优质的博文、最精彩的讨论、最实用的开发资源;提供最新最全的编程学习资料:PHP、Objective-C、Java、Swift、C/C++函数库、.NET Framework类库、J2SE API等等。并不定期奉送各种福利。
目录
相关文章推荐
程序员小灰  ·  百度进军娱乐圈?这次破圈的是百度网盘... ·  3 天前  
Java知音  ·  真的建议赶紧搞个软考证书!(红利期) ·  4 天前  
程序员小灰  ·  不愧是字节跳动,今年这薪资... ·  5 天前  
51好读  ›  专栏  ›  程序员大咖

关于 JavaScript 的21个问题集锦

程序员大咖  · 公众号  · 程序员  · 2017-03-09 19:34

正文

Beginning

关于JavaScript,工作和学习过程中遇到过许多问题,也解答过许多别人的问题。这篇文章记录了一些有价值的问题。

1. 对象字面值不能正确解析

问题{a:1}.a报错,错误Uncaught SyntaxError: Unexpected token .

解决

({a:1}.a) // 或({a:1}).a

原因

MDN: Object literals

An object literal is a list of zero or more pairs of property names and associated values of an object, enclosed in curly braces ({}). You should not use an object literal at the beginning of a statement. This will lead to an error or not behave as you expect, because the { will be interpreted as the beginning of a block.

简单说,就是声明对象字面值时,语句开头不应该用{,因为js解释器会认为这是语句块(block)的开始。

同理,类似问题{ name: "mc", id: 1 }会报错Uncaught SyntaxError: Unexpected token :也是这个道理。({ name: "mc", id: 1 })即可正确解析。但稍注意下,{name: "mc"}是不会报错的,它等同于name: "mc",并返回一个字符串"mc"

2. 数字的点操作符

问题123.toFixed(2)报错,错误Uncaught SyntaxError: Unexpected token ILLEGAL

解决

(123).toFixed(2) // >> "123.00"
// 以下两种都可以,但完全不推荐
123..toFixed(2)
123 .toFixed(2)

原因

很简单,js解释器会把数字后的.当做小数点而不是点操作符。

3. 连等赋值问题

问题:尝试解释下连等赋值的过程。下面的代码为什么是这样的输出?

var a = {n: 1};  
var b = a; a.x = a = {n: 2};
console.log(a.x);// --> undefined  
console.log(b.x);// --> {n:2}

原因

我们可以先尝试交换下连等赋值顺序(a = a.x = {n: 2};),可以发现输出不变,即顺序不影响结果。

那么现在来解释对象连等赋值的问题:按照es5规范,题中连等赋值等价于
a.x = (a = {n: 2});,按优先获取左引用(lref),然后获取右引用(rref)的顺序,a.xa中的a都指向了{n: 1}。至此,至关重要或者说最迷惑的一步明确。(a = {n: 2})执行完成后,变量a指向{n: 2},并返回{n: 2};接着执行a.x = {n: 2},这里的a就是b(指向{n: 1}),所以b.x就指向了{n: 2}

搜索此题答案时,颜海镜的一篇博客关于此题也有讲述,不过没有讲清楚(或许是我没有领会 :P)。

4. 逗号操作符

问题: 下面的代码返回什么,为什么?

var x = 20;
var temp = {    x: 40,    
   foo: function() {
       
var x = 10;
       
return this.x;    } }; (temp.foo, temp.foo)(); // 20,而不是40

原因

MDN逗号操作符:

The comma operator evaluates each of its operands (from left to right) and returns the value of the last operand.

即逗号操作符会从左到右计算它的操作数,返回最后一个操作数的值。所以(temp.foo, temp.foo)();等价于var fun = temp.foo; fun();fun调用时this指向window,所以返回20。

5. parseInt传入数字

问题: parseInt传入数字时为什么有以下输出?

parseInt(0.000008) // >> 0
parseInt(0.0000008) // >> 8

原因

parseInt(arg)时会调用arg.toString()

(0.000008).toString() // "0.000008"
(0.0000008).toString() // "8e-7"

6. 前端面试题,利用给定接口获得闭包内部对象

问题: 前端面试题,利用给定接口获得闭包内部对象

var o = (function() {    var person = {
        name: 'Vincent',
        age: 24,
    }; 
   
return {
       
run: function(k) {
           
return person[k];        },    } }());

在不改变上面的代码情况下, 怎么得到原有的 person 对象?

解决

Object.defineProperty(Object.prototype, 'self', 
    {
       
get: function() {
           
return this;        },        configurable: true    });
o.run('self'); // 输出 person

但如果加上person.__proto__ = null,目前还没找到解决方法。

7. 原型链导致的属性更改无效

问题: 看下面的代码,为什么obj.x = 100的赋值无效?

var proto = Object.create({}, {
    x: {
        value: 1,
        writable: false
    }
});var obj = Object.create(proto);
obj.x = 100; // 无效,strict mode下报错: Uncaught TypeEr
console.log(obj.x); // 1

原因

es5 assignment

When an assignment occurs within strict mode code, its LeftHandSide must not evaluate to an unresolvable reference. If it does a ReferenceError exception is thrown upon assignment. The LeftHandSide also may not be a reference to a data property with the attribute value {[[Writable]]:false}, to an accessor property with the attribute value {[[Set]]:undefined}, nor to a non-existent property of an object whose [[Extensible]] internal property has the value false. In these cases a TypeError exception is thrown.

本题中,obj.x实际指向proto.xwritable: false阻止了赋值。

8. 位操作符

问题: 实现浮点数转整数,或者说取出数字的整数部分。比如-12.921 --> -1212.921 --> 12等等。

解决

function convertToInt(num) {
     
return num >> 0; }
convertToInt(-Math.PI); // -3
convertToInt(12.921); // 12

原因

没有什么高达上,就是神奇的位操作符:有符号右移>>The Signed Right Shift Operator ( >> ) 

...
5. Let lnum be ToInt32(lval).
...

本题利用了有符号右移会将左操作数转换为32位整数。

补充

同理,num | 0也是可以的。

9. IEEE-754精度问题

问题: 0.1 + 0.2 !== 0.3 // true

原因

所有使用IEEE-754数字实现的编程语言都有这个问题。

0.10.2的二进制浮点数表示并不是精确的,所以相加后不等于0.3。这个相加的结果接近0.30000000000000004

那么你一定想要比较相加后的结果和预期数字怎么办?宽容比较,即允许一定误差,这个误差值一般是2^-52 (2.220446049250313e-16)。

update 2017-02-13: ES2015标准化了这个值,可通过Number.EPSILON访问

10. Function.prototype.call/apply 的 this 问题

问题: 下列情况中this为什么是这样的?

原因

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call

The value of this provided for the call to fun. Note that this may not be the actual value seen by the method: if the method is a function in non-strict mode code, null and undefined will be replaced with the global object, and primitive values will be boxed.

非严格模式下,this默认指向全局对象,call/apply显式指定this参数时也会强制转换参数为对象(如果不是对象)。其中,null/undefined被替换为全局对象,基础类型被转换为包装对象。

严格模式下,this默认为undefined,且call/apply显式指定this参数时也不会有强制转换。

11. 给基础类型添加属性无效

问题: 为什么给基础类型添加属性不报错但又无效?

var num = 1;
num.prop = 2;
num.prop // undefined

原因

Property Accessors规范 https://es5.github.io/#x11.2.1:

2.Let baseValue be GetValue(baseReference).

GetValue规范 https://es5.github.io/#x8.7.1:

4.If IsPropertyReference(V), then ...

最后可知,num.prop 等同于 Object(num).prop,也就是说我们添加属性到一个新建的对象上,与num没任何关系。

12. 数组的展开/扁平

问题: [1,2,[2,3,[4,5]]]--->[1,2,2,3,4,5]

function flatten(arr) {
   
if(!isArray(arr) || !arr.length) {
       
return [];    } else {
        
return Array.prototype.concat.apply([], arr.map(function(val) {            return isArray(val) ? flatten(val) : val;        }));    }    

   function isArray(arr) {
       
return Object.prototype.toString.call(arr).slice(8, -1).toLowerCase() === 'array';    } }flatten([1,2,[2,3,[4,5]]]);
// [1, 2, 2, 3, 4, 5]

另外,看到一种方法:利用array.toString()然后重新解析也可以完成,但此时数组元素类型丢失。这种方法利用了ToString(array)会转换成"x,y,z..."

13. jQuery.triggerextraParametersundefined的问题

问题: 下面slider为什么是undefined

刚看到错误时感觉莫名其妙:怎么就错了?jQuery用的很 6 好不好?😂

调试!打了几个断点,找到了原因:

data = data != null ? jQuery.makeArray( data ) : [];
data.unshift( event );

trigger中的self会被makeArray处理,这本来没什么,坑爹的是 self有个属性: self.length = 100,然后悲剧了,makeArray之后data变成[undefined, undefined, ...]

好了,debug完毕,问题看起来很简单,但总结出个道理:错误常常出现在你想不到/忽视的地方。

14. delete操作符

问题: 试着解释下面代码的结果?

(function(x){
   
delete x;
    
return x; })(1);
// 返回 1

在QQ群看到这个问题,想想还是有点坑在里面的。翻了翻MDN,加深/重新记忆下对delete的理解。

delete是删除对象的属性。严格模式下,删除non-configurable的属性报错,非严格则返回false,其它任何情况都返回true

回到本题,delete x其实是尝试删除局部变量x局部变量是non-configurable,所以无法删除,在严格模式下,题中代码将报错。

更正:对局部变量和函数名delete是无效的,delete只能删除属性。delete obj.propName 才是合法的形式。下面以代码详细解释:

15. jQuery.fn.offset不支持获取和设置display:none元素的坐标

不支持获取很好理解,display:none元素不在文档流中,获取位置自然是undefined。而$(hiddenElement).offset()会返回{top: 0, left: 0}以提高程序健壮性。

不过问题核心是为什么无法正确设置display:none元素的坐标,具体点,即为什么**$(hiddenElement).offset(options)设置的真实值是指定值的两倍**?

如上面代码所示,隐藏的元素实际被设置打坐标是给定值的2倍,为什么?来看下jQuery关于offset部分的源码:

原因就是$el.offset(options)并不简单采用options.leftoptions.top,而会计算( options.top - curOffset.top ) + curTop作为topleft同理)。隐藏元素的 css属性部分的left和top可以正常取到,但curOffset则是{top: 0, left: 0},所以( options.top - curOffset.top ) + curTop --> options.top + curTop造成一倍误差

16. 找出字符串中出现最多的字母

这个问题看起来用到的地方挺多,至少我遇到过不止一次,索性在这里讲一讲。先具体描述下问题:

假设字符串'ababccdeajxac',请找出出现次数最多的字符?

最先想到的解法是用map纪录每个字符的次数,然后找出最多的即可:

此外,可以考虑用正则来辅助处理:

17. storage event

当你用localStoragesessionStorage的API去更改Storage时,会触发storage事件:


这里没有什么特别的,但基本所有问题的根源,或者说要特别注意的是:本页面更改Storage只能在同域名的其它页面去捕获storage事件。

The StorageEvent is fired whenever a change is made to the Storage object. This won't work on the same page that is making the changes — it is really a way for other pages on the domain using the storage to sync any changes that are made. Pages on other domains can't access the same storage objects.

18. 一个函数柯里化问题及更多

C君出了这样一个题,要求实现sum函数如下:

sum(1) // 1
sum(1)(2) // 3
sum(1)(2)(3) // 6

第一眼,这不是函数柯里化吗,小case,然后我挥笔写下:

function sum(item) {
   
var cur = item;
    
var inner = function(next) {
       
return next == null ? cur : (cur += next, inner);    };
    
return item == null ? undefined : inner; }

运行下:

perfect!

然后我看了看答案,又看了看题目 😕 

好吧,题目根本没有最后的()。所以最后答案是:

如上,改写toString达成魔法效果。

19. 运算符优先级问题

普通的运算符优先级对大家应该不成问题,另外也推荐用括号来明确运算符优先级和代码意图,写出更可读的代码

但之所以有这个问题,是涉及到**typeof**运算符,比较新颖。

问题:

var str = 'why I am ' + typeof + ''; // so what is str?

strwhy I am number,思考一下,上面的代码应该等同于'why I am ' + (typeof (+ ''))。好,现在问题归结到运算符优先级,查看文档验证:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence

typeof运算符优先级高于+,并且是right-to-left

20. Prefix Increment Operator(++)的问题

关于前自增运算符的一个有意思的问题:

++'52'.split('')[0] //返回的是?

这道题来自Another JavaScript quiz第8题,主要是优先级问题,应该返回6,看完答案应该没什么难理解的。但是,题目的某个注意点:

却非常有意思。所以问题是为什么++'5'报错而++'52'.split('')[0]可以正确执行?

阅读http://es5.github.io/#x11.4.4,可以看到_Prefix Increment Operator_操作的第5步PutValue(expr, newValue)要求expr是引用。

而在这里,

  • '5'是值,不是引用,所以报错。

  • '52'.split('')[0]返回的是['5','2'][0],对象的属性访问返回的是引用,所以可以正确执行。

21. 你真的了解String.prototype.split吗?


上面代码段的输出使我很疑惑(什么鬼)... 问题简化下,就是split所用的正则有捕获时怎么处理?

查API,str.split([separator[, limit]]):

separator: Optional. Specifies the character(s) to use for separating the string. The separator is treated as a string or a regular expression. If separator is omitted, the array returned contains one element consisting of the entire string. If separator is an empty string, str is converted to an array of characters.

If separator is a regular expression that contains capturing parentheses, then each time separator is matched, the results (including any undefined results) of the capturing parentheses are spliced into the output array. However, not all browsers support this capability.

按API,var result = str.split(separator);

separator是带捕获的正则时,每次命中,都添加到结果数组result中。




作者:creeperyang

链接:https://github.com/creeperyang/blog/issues/2

来源:GitHub