0%

Js深度思考(主要是坑)

一些自己遇到的坑、一些别人遇到的坑、一些有意思的现象

定时器

关于setTimeout误差的进一步了解:
这里默认大家都知道setTimeout为延时执行,
在控制台输入以下代码,执行结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var value = 1;
var start = Date.now();
var timerId = setTimeout(function run() {

if(value > 200) {
clearTimeout(timerId);
return;
}
++value;
var now = Date.now();
console.log(value,now-start);
start = Date.now();
timerId = setTimeout(run, 1000)
}, 1000)

结果

不是说好的延时执行吗?怎么还有999的数据?

解析
即使天时地利人和,到了定时的时间时,JS主线程空闲,异步任务队列中只有setTimeout执行的方法,这个方法的执行时间也并不是精确的delay时间(精确到毫秒),因为浏览器上的计时器精确度有限:《Javascript高级程序设计(第三版)》有写

promise的一些现象

值穿透:

then的参数不是函数时(eg. 2、null、Promise.resolve(3))会跳过去发生值穿透,最终表达式返回的Promise中的值为最后执行then中函数的返回值。
值穿透时因为回调队列中不会搜集处理数值、表达式等。

正常返回

穿透返回

输出的Promise是最后一个then返回的promise

Catch异常:

执行then的过程有异常发生时后边的then不会执行,第一个then产生异常,第二个then没有异常回调,所以异常直接被catch捕获到(回调队列中成功、失败回调是顺序收集、执行的)

1
2
3
4
5
6
7
8
9
10
11
Promise.resolve(1)
.then((res) => {
console.log(res);
return 2;
})
.catch((err) => { // 没有成功回调,上边的resolve值直接往下传(回调队列中成功、失败回调是顺序收集、执行的)
return 3;
})
.then((res) => {
console.log(res);
});

结果
1 2
解析
Promise首先resolve(1),接着就会执行then函数,因此会输出1,然后在函数中返回2。
因为是resolve函数,因此后面的catch函数不会执行,而是直接执行第二个then函数,因此会输出2。

Promise 构造函数只执行一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('开始');
resolve('success');
}, 5000);
});

const start = Date.now();
promise.then((res) => {
console.log(res, Date.now() - start);
});

promise.then((res) => {
console.log(res, Date.now() - start);
});

结果
开始
success 5002
success 5002  
解析
promise 的.then或者.catch可以被调用多次,但这里 【Promise 构造函数只执行一次】。
或者说 promise 内部状态一经改变,并且有了一个值,那么后续每次调用.then 或者.catch都会直接拿到该值。
Promise状态一旦改变,无法在发生变更。

再看运算符

运算符优先级

1
2
var val = 'smtg';
console.log('Value is ' + (val === 'smtg') ? 'Something' : 'Nothing');

结果:Something
解析
字符串连接比三元运算有更高的优先级
所以原题等价于'Value is true' ? 'Somthing' : 'Nonthing'
而不是 'Value is' + (true ? 'Something' : 'Nonthing')
巩固

1
2
1 || fn() && fn()   //1  
1 || 1 ? 2 : 3 ; //2

巩固的解释请看下面这篇文章
要点:绑定、关联、短路
Like Sunday, Like Rain - JavaScript运算符优先级之谜

运算符运算

1
2
'5' + 3
'5' - 3

结果:53 2
解析:加号有拼接功能,减号就是逻辑运算
巩固:typeof (+”1”) // “number” 对非数值+—常被用来做类型转换相当于Number()

1
1 + - + + + - + 1

结果:2
解析:+-即是一元加和减操作符号,又是数学里的正负号。负负得正哈。
巩固: 一元运算符还有一个常用的用法就是将自执行函数的function从函数声明变成表达式。
常用的有 + - ~ ! void
+ function () { }
- function () { }
~ function () { }
void function () { }

高阶函数相关

1
2
3
var ary = [0,1,2];
ary[10] = 10;
ary.filter(function(x) { return x === undefined;});

结果:[]
解析
数组的值 ary = [0, 1, 2, empty × 7, 10];
filter() 不会对空数组进行检测。会跳过那些空元素
巩固

1
2
3
var ary = [0,1,2,undefined,undefined,undefined,null];
ary.filter(function(x) { return x === undefined;});
// [undefined, undefined, undefined]

扩展

1
2
3
var ary = Array(3);
ary[0]=2
ary.map(function(elem) { return '1'; });

结果:[“1”, empty × 2]
解析:如过没有值,map会跳过不会执行回调函数

类型转换

test1:

demo1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function showCase(value) {
switch(value) {
case 'A':
console.log('Case A');
break;
case 'B':
console.log('Case B');
break;
case undefined:
console.log('undefined');
break;
default:
console.log('Do not know!');
}
}
showCase(new String('A'));

结果:Do not know!
解析switch判断的是全等(===) ,new String(x)是个对象
巩固
var a = new String(‘A’) ;
a.proto // String.prototype 实例的原型指向构造函数的原型对象

demo2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function showCase2(value) {
switch(value) {
case 'A':
console.log('Case A');
break;
case 'B':
console.log('Case B');
break;
case undefined:
console.log('undefined');
break;
default:
console.log('Do not know!');
}
}
showCase2(String('A'));

结果:Case A
解析:String(‘A’)就是返回一个字符串
巩固:
var a1 = String(‘A’);
var a2 = ‘A’;
a2.proto // String.prototype
a1.proto === a2.proto // true 上一题的a.proto

那字符串不是对象为啥也指向String.prototype?
解析:a2是基本类型的值,逻辑上不应该有原型和方法。为了便于操作,有一种特殊的引用类型(基本包装类型)String。其实读取时,后台会自动完成下面的操作:
var str = new String(“A”); //创建实例
str.proto; //调用指定属性和方法
str = null; //销毁实例
所以 a1.proto === a2.proto
但注意基本包装类型特殊就在于它对象(str)的生命周期,只存在于一行代码(a1.proto === a2.proto)的执行瞬间。
这也就解释了为啥字符串也能操作属性和方法但不能添加。基本包装类型有三个(String,Number,Boolean)
(详情请看《js高程》 5.6基本包类型 P119)

test2:

1
2
3
4
5
6
var a = [0];
if ([0]) {
console.log(a == true);
} else {
console.log("wut");
}

结果:false
解析
[0]的boolean值是true
console.log(a == true); // 转换为数字进行比较, Number([0]) => 0 ,
Number(true) => 1 ,所有是false
巩固
2 == [[[2]]] // true

test3:

1
2
3
4
5
6
7
8
9
10
11
function isOdd(num) {
return num % 2 == 1;
}
function isEven(num) {
return num % 2 == 0;
}
function isSane(num) {
return isEven(num) || isOdd(num);
}
var values = [7, 4, '13', -9, Infinity];
values.map(isSane);

结果:[true, true, true, false, false]
解析
%如果不是数值会调用Number()去转化
‘13’ % 2 // 1
Infinity % 2 //NaN Infinity 是无穷大
-9 % 2 // -1
巩固: 9 % -2 // 1 余数的正负号随第一个操作数

test4:

1
[1 < 2 < 3, 3 < 2 < 1]

结果:[true,true]
解析
1 < 2 => true;
true < 3 => 1 < 3 => true;

3 < 2 => false;
false < 1 => 0 < 1 => true;

test5:

1
Array.isArray( Array.prototype )

结果:true
解析
Array.prototype是一个数组
数组的原型是数组,对象的原型是对象,函数的原型是函数

原型

1
2
var a = {}, b = Object.prototype;
[a.prototype === b, Object.getPrototypeOf(a) === b]

结果:false, true
解析:Object 的实例是 a,a上并没有prototype属性
a的__poroto__ 指向的是Object.prototype,也就是Object.getPrototypeOf(a)。a的原型对象是b
巩固

1
2
3
function f() {}
var a = f.prototype, b = Object.getPrototypeOf(f);
a === b

结果:false
解析:a是构造函数f的原型 : {constructor: ƒ}
b是实例f的原型对象 : ƒ () { [native code] }

暂时没有归类的项

test1:

1
2
3
4
5
var two   = 0.2
var one = 0.1
var eight = 0.8
var six = 0.6
[two - one == one, eight - six == two]

结果:[true, false]
解析:IEEE 754标准中的浮点数并不能精确地表达小数
巩固

1
2
3
4
5
6
var two   = 0.2;
var one = 0.1;
var eight = 0.8;
var six = 0.6;
( eight - six ).toFixed(4) == two
//true

test2:

1
2
3
3.toString()
3..toString()
3...toString()

结果:error, “3”, error
解析:因为在 js 中 1.1, 1., .1 都是合法的数字. 那么在解析 3.toString 的时候这个 . 到底是属于这个数字还是函数调用呢? 只能是数字, 因为3.合法啊!

test3:

1
2
3
4
5
(function(){
var x = y = 1;
})();
console.log(y);
console.log(x);

结果:1, error
解析:y 被赋值成全局变量,等价于
y = 1 ;
var x = y;

test4:

1
2
3
4
5
6
7
var a = [1, 2, 3],
b = [1, 2, 3],
c = [1, 2, 4]
a == b
a === b
a > c
a < c

结果:false, false, false, true
解析:相等(==)和全等(===)还是比较引用地址
引用类型间比较大小是按照字典序比较,就是先比第一项谁大,相同再去比第二项。

test5:

1
2
3
4
function foo() { }
var oldName = foo.name;
foo.name = "bar";
[oldName, foo.name]

结果:[“foo”, “foo”]
解析:函数的名字不可变.

test6:

1
[,,,].join(",")

结果:”,,”
解析:因为javascript 在定义数组的时候允许最后一个元素后跟一个,
所以这个数组长度是3,
巩固: [,,1,].join(“.”).length // 3

test7:

1
2
3
4
5
6
7
8
9
function foo(a) {
var a;
return a;
}
function bar(a) {
var a = 'bye';
return a;
}
[foo('hello'), bar('hello')]

结果:[“hello”, “bye”]
解析:变量声明

test8:

1
2
3
4
5
6
7
8
9
var name = 'World!';
(function () {
if (typeof name === 'undefined') {
var name = 'Jack';
console.log('Goodbye ' + name);
} else {
console.log('Hello ' + name);
}
})();

答案:Goodbye Jack
解析:(1)typeof时 name变量提升。 在函数内部之声明未定义
(2)typeof优先级高于===
巩固

1
2
3
4
5
6
7
8
9
10
var str = 'World!';   
(function (name) {
if (typeof name === 'undefined') {
var name = 'Jack';
console.log('Goodbye ' + name);
} else {
console.log('Hello ' + name);
}
})(str);
// 答案:Hello World 因为name已经变成函数内局部变量