JavaScript常见问题和解决方式
在使用JavaScript的时候常常会碰到各种奇葩的问题,这篇文章整理了近十个常见的问题,以及其对应的解决方式。不过除了这些问题之外,另外有两篇外文整理了更多的相关问题,这篇文章取于当中一些更为有意义的问题。感兴趣的同学可以看看,也可以将你碰到的一些奇葩问题在评论中一起分享。
类型判断
使用 typeof
运算符判断一个原始值变量的类型是没有问题的,但如果判断的是引用值类型就会有局限性,比如 null
和数组的结果都是 object
。要想判断变量属于哪种内置类型,最靠谱的方式是调用 Object.prototype.toString
方法:
Object.prototype.toString.call([])
// => "[object Array]"
在 JavaScript 规范(ECMA-262 19.1.3.6)中详细解释了 Object.prototype.toString
的解析过程:
- 01.如果
this
的值为undefined
,则返回字符串"[object Undefined]"
- 02.如果
this
的值为null
,则返回字符串"[object Null]"
- 03.使用
O
表示ToObject(this)
的值 - 04.使用
isArray
表示IsArray(O)
的值 - 05.如果
isArray
不是正常值(比如抛出错误),则中断执行 - 06.如果
isArray === true
,则builtinTag = "Array"
,之后执行第 16 步 - 07.如果
O
是一个exotic
字符串对象,则builtinTag = "String"
,之后执行第 16 步 - 08.如果
O
拥有内部属性[[ParameterMap]]
,则builtinTag = "Arguments"
,跳到第 16 步 - 09.如果
O
拥有内部方法[[Call]]
,则builtinTag = "Function"
,跳到第 16 步 - 10.如果
O
拥有内部属性[[ErrorData]]
,则builtinTag = "Error"
,跳到第 16 步 - 11.如果
O
拥有内部属性[[BooleanData]]
,则builtinTag = "Boolean"
,跳到第 16 步 - 12.如果
O
拥有内部属性[[NumberData]]
,则builtinTag = "Number"
,跳到第 16 步 - 13.如果
O
拥有内部属性[[DateValue]]
,则builtinTag = "Date"
,跳到第 16 步 - 14.如果
O
拥有内部属性[[RegExpMather]]
,则builtinTag = "RegExp"
,跳到第 16 步 - 15.如果第 6 ~ 14 步都不符合,则
builtinTag = "Object"
,跳到第 16 步 - 16.使用
tag
表示Get (O, @@toStringTag)
的值 - 17.如果
tag
不是正常值,则中断执行 - 18.如果
Type(tag)
不是一个字符串,则tag = builtinTag
- 19.返回一个
"[object" + tag + "]"
形式的字符串
开发者喜欢使用该方法获取内部属性
[[Class]]
的字符串值,用于检测内建对象的类型。值得注意的是,这一方法只对内建对象有效,对宿主对象等其他类型的对象则不具有可信度。
局部变量泄漏到全局
有如下所示的代码,猜测一下输出结果:
(function(){
var a = b = 3;
})();
console.log(a);
console.log(b);
很多时候不经意间,某些变量就会泄漏为全局变量,比如这里的变量 b
。上述代码等同于:
(function(){
b = 3;
var a = b;
})();
ES6 支持块级作用域,使用时需要遵循两个条件:一是添加 "use strict;"
字符串,声明严格模式;而是使用 let
和 const
声明变量。如果不能使用 ES6 的块级作用域,那么声明变量时要做到使用单独一行声明单个变量,比如:
var b = 3;
var a = b;
立即执行函数
使用立即执行函数的好处是模块化和块级作用域。目前 ES6 对这两个方面都有相应的支持,所以除非为了保持兼容性,ES6 会是更好的开发方式。
在浏览器环境中,立即调用函数中的 this
指向全局变量 window
:
(function () {
console.log(this);
console.log(this === window);
// => true
})();
严格模式
- 使用
'use strict;'
声明严格模式 - 全局变量必须显示声明
- 禁止使用
with
语句,限制动态绑定 eval()
的作用域为独立作用域,独立于全局作用域和函数作用域之外,生成的变量只能用于eval
内部- 禁止
this
关键字指向全局对象 - 禁止使用
delete
删除变量,只有configurable === true
的属性可以被删除 - 对可读属性赋值、对禁止扩展的对象添加属性、删除不可删除属性都会报错
- 禁止在对象中添加重名属性
- 不允许对
arguments
赋值 - 禁止使用
arguments.callee
- 必须在顶层作用域声明函数
- 新增保留字
浮点数的精度问题
对于使用 IEEE 754 存储双精度 64 位浮点数的语言都会遇到这个问题,且最简单的复现方式就是计算 0.1 + 0.2
。这一表达式的值不等于 0.3
,其原因就是 IEEE 754 不能正确表示 0.1
。IEEE 754 规定的浮点数由一位符号位 s
、五十二位小数位 m
和十一位指数位组成 e
:
$$s \times m \times 2^e$$
JavaScript 规定 e
的范围为 [-1074, 971]
,则 Number.MAX_VALUE
的值为:
$$ 1 \times ( 2^{53} - 1) \times 2^{971} $$
了解以上基础知识后求取 0.1
和 0.2
的二进制表示:
(0.1).toString(2)
// => "0.0001100110011001100110011001100110011001100110011001101"
(0.2).toString(2)
// => "0.001100110011001100110011001100110011001100110011001101"
(0.1 + 0.2).toString(2)
// => "0.0100110011001100110011001100110011001100110011001101"
关于这一问题的详细解析和其他问题,建议参考文章:
setTimeout
JavaScript 是单线程语言,异步事件的优先级低于其他代码,setTimeout(callback, 0)
表示加入事件队列以异步的方式执行,且在事件队列中优先执行:
(function() {
console.log(1);
setTimeout(function(){console.log(2)}, 1000);
setTimeout(function(){console.log(3)}, 0);
console.log(4);
})();
Object key
var a={},
b={key:'b'},
c={key:'c'};
a[b]=123;
a[c]=456;
console.log(a[b]);
// => 456
上面的代码有一个特点,就是没有使用字符串或 Symbol
作为对象的属性名,所以系统会将其转换为字符串,相当于:
a[Object.prototype.toString.call(b)]=123;
a[Object.prototype.toString.call(c)]=456;
// equal to
a["[object Object]"]=123;
a["[object Object]"]=456;
eval 作用域
var y = 1;
if (function f(){}) {
y += typeof f;
}
console.log(y);
// => "1undefined"
在上面的代码中,最奇怪的有点在于 typeof f === "undefined"
。之所以有这样的结果,是因为 if 条件语句是使用 eval()
解析的。由于 eval(function f(){})
只会返回值而不会向全局作用域暴漏变量 f
,所以执行 typeof f
时并没有找到函数 f
,则值为 undefined
。
私有方法
在 JavaScript 中模拟私有方法的缺点就是内存占用高:
var Employee = function (name, company, salary) {
this.name = name || "";
this.company = company || "";
this.salary = salary || 5000;
// Private method
var increaseSalary = function () {
this.salary = this.salary + 1000;
};
// Public method
this.dispalyIncreasedSalary = function() {
increaseSlary();
console.log(this.salary);
};
};
var emp1 = new Employee("John","Pluto",3000);
var emp2 = new Employee("Merry","Pluto",2000);
var emp3 = new Employee("Ren","Pluto",2500);
对于 Employee
的每一个实例 emp1
/ emp2
/ emp3
,它们都拥有各自的一个 increaseSalary
方法,所以除非确有必要,尽量不要使用私有方法。
delete 操作符
var output = (function(x){
delete x;
return x;
})(0);
console.log(output);
// => 0
delete
操作符用于删除对象的属性,对于其他变量无效。
var Employee = {
company: 'xyz'
}
var emp1 = Object.create(Employee);
delete emp1.company;
console.log(emp1.company);
// => "xyz"
在上面的代码中,emp1
通过原形链继承了 Employee
的属性 company
,emp1
本身并没有 company
属性,即 emp1.company === emp1.__proto__.company
,所以 delete
操作是无效的。
延迟定义
// 第一种声明方式
var foo = function(){
// Some code
};
// 第二种声明方式
function bar(){
// Some code
};
这两种声明方式的差异在于,foo()
在运行时定义,而 bar()
在解析时定义。
foo();
// 执行 foo() 时,foo 的值为 undefined
var foo = function(){
console.log("Hi I am inside Foo");
};
bar();
// 执行 bar() 时,由于 bar() 已经被 JavaScript 引擎解析过了,所以不会报错
function bar(){
console.log("Hi I am inside Foo");
};
根据这一特点,我们可以根据需要延迟定义某些功能:
if(testCondition) {
var foo = function(){
console.log("inside Foo with testCondition True value");
};
}else{
var foo = function(){
console.log("inside Foo with testCondition false value");
};
}
在上面的代码中,布尔值 testCondition
决定了是定义何种功能的 foo()
函数,在定义之前,foo
只是一个原始值 undefined
,并没有引用具体的函数。
参考资料
- 25 Essential JavaScript Interview Questions
- 21 Essential JavaScript Tech Interview Practive Questions answers
如需转载,烦请注明出处:https://www.fedev.cn/javascript/interview-questions.html
Air Force 1 Ultra Flyknit 2.0 - Black