js能力提升2
# ['1', '2', '3'].map(parseInt) what & why
原文链接:木易杨前端进阶 (opens new window)
下面🌰输出结果是什么1:
['10','10','10','10','10'].map(parseInt);
# parseInt
parseInt()
函数解析一个字符串参数,并返回一个指定基数的整数 。
const intValue = parseInt(string[, radix]);
string
要被解析的值。如果参数不是一个字符串,则将其转换为字符串(使用 ToString 抽象操作)。字符串开头的空白符将会被忽略。
radix
一个介于2和36之间的整数(数学系统的基础),表示上述字符串的基数。默认为10。 返回值
返回一个整数或NaN
parseInt(100); // 100
parseInt(100, 10); // 100
parseInt(100, 2); // 4 -> 将基数 2 中的 100 转换为基数 10
2
3
注意: 在radix
为 undefined,或者radix
为 0 或者没有指定的情况下,JavaScript 作如下处理:
- 如果字符串 string 以"0x"或者"0X"开头, 则基数是16 (16进制).
- 如果字符串 string 以"0"开头, 基数是8(八进制)或者10(十进制),那么具体是哪个基数由实现环境决定。ECMAScript 5 规定使用10,但是并不是所有的浏览器都遵循这个规定。因此,永远都要明确给出radix参数的值。
- 如果字符串 string 以其它任何值开头,则基数是10 (十进制)。
更多详见parseInt | MDN (opens new window)
# 总结
['1', '2', '3'].map(parseInt)
对于每个迭代map
, parseInt()
传递两个参数: 字符串和基数。 所以实际执行的的代码是:
['1', '2', '3'].map((item, index) => {
return parseInt(item, index)
})
2
3
即返回的值分别为:
parseInt('1', 0) // 1
parseInt('2', 1) // NaN
parseInt('3', 2) // NaN, 3 不是二进制
2
3
所以:
['10','10','10','10','10'].map(parseInt);
// [10, NaN, 2, 3, 4]
2
等价于
['10', '10', '10', '10', '10'].map((item, index) => {
return parseInt(item,index)
})
2
3
所返回的值分别为:
parseInt('10', 0) // 10
parseInt('10', 1) // NaN,没有1进制
parseInt('10', 2) // 2
parseInt('10', 3) // 3
parseInt('10', 4) // 4
2
3
4
5
# 在对象中加入数组的属性,对象会变成一个类数组
下面🌰的输出结果是什么?
var obj = {
'2': 3,
'3': 4,
'length': 2,
'splice': Array.prototype.splice,
'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)
2
3
4
5
6
7
8
9
10
结果是:
Object(4) [empty x 2, 1, 2, splice: f, push f]
2:1
3:2
length:4
push:f push()
splice:f splice()
...
2
3
4
5
6
7
# 类数组(ArrayLike)
一组数据,由数组来存,但是如果要对这组数据进行扩展,会影响到数组原型,ArrayLike的出现则提供了一个中间数据桥梁,ArrayLike有数组的特性, 但是对ArrayLike的扩展并不会影响到原生的数组。
# push方法
push 方法有意具有通用性。该方法和 call() 或 apply() 一起使用时,可应用在类似数组的对象上。push 方法根据 length 属性来决定从哪里开始插入给定的值。如果 length 不能被转成一个数值,则插入的元素索引为 0,包括 length 不存在时。当 length 不存在时,将会创建它。 唯一的原生类数组(array-like)对象是 Strings,尽管如此,它们并不适用该方法,因为字符串是不可改变的。
# 总结
可以说,在对象中加入splice和length属性后,这个对象变成一个类数组。
所以题目的解释应该是:
- 使用第一次push,obj对象的push方法设置
obj[2]=1;obj.length+=1
- 使用第二次push,obj对象的push方法设置
obj[3]=2;obj.length+=1
- 使用console.log输出的时候,因为obj具有 length 属性和 splice 方法,故将其作为数组进行打印
- 打印时因为数组未设置下标为 0、1 处的值,故打印为empty,主动 获取 obj[0]的值为 undefined
# 关于对象键名的转换
下面🌰的输出结果是什么?
// example 1
var a={}, b='123', c=123;
a[b]='b';
a[c]='c';
console.log(a[b]);
---------------------
// example 2
var a={}, b=Symbol('123'), c=Symbol('123');
a[b]='b';
a[c]='c';
console.log(a[b]);
---------------------
// example 3
var a={}, b={key:'123'}, c={key:'456'};
a[b]='b';
a[c]='c';
console.log(a[b]);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
这题考察的是对象键名的转换。
键名转换有以下规则:
- 对象的键名只能是字符串和Symbol类型。
- 其它类型的键名会被转换成字符串类型。
- 对象转字符串默认会调用toString方法。
所以输出结果为:
// example 1
var a={}, b='123', c=123;
a[b]='b';
a[c]='c'; // c 的键名会被转换为字符串'123',这里会把 b 覆盖掉
console.log(a[b]); // 输出 c
2
3
4
5
// example 2
var a={}, b=Symbol('123'), c=Symbol('123');
a[b]='b'; // b 的键名是 Symbol 类型不需要转换
a[c]='c'; // c 的键名是 Symbol 类型不需要转换,并且任何一个Symbol类型的值都是不相等的,所以不会覆盖 b
console.log(a[b]); // 输出 b
2
3
4
5
// example 3
var a={}, b={key:'123'}, c={key:'456'};
a[b]='b'; // b 的键名是对象类型,会调用 toString 方法转换为字符串 [object Object]
a[c]='c'; // 同上,所以这里会把 b 覆盖掉
console.log(a[b]); // 输出 c
2
3
4
5
# input框如何处理中文输入
看过element-ui框架源码的童鞋应该都知道(我没看过),element-ui是通过 compositionstart
&compositionend
做的中文输入处理:
// 相关代码:
<input
ref="input"
@compositionstart="handleComposition"
@compositionupdate="handleComposition"
@compositionend="handleComposition"
>
2
3
4
5
6
7
这三个方法都是原生的方法,这里简要说一下这三个方法的执行流程:
输入英文是不会触发这三个方法的,只有输入中文才可以。还在输入拼音时(此时input还没有填入内容),会首先触发 compositionstart
事件,然后每打一个拼音字母就会触发一次 compositionupdate
事件,最后将输入好的中文填入input时会触发 compositionend
事件。触发 compositionstart
的时候,文本框会填入“虚拟文本”(待确认文本),同时触发input事件;在触发 compositionend
时,就是填入实际内容后(已确认文本)。
如下图所示:
我们可以从上图中看到:
- 识别到你开始使用中文输入法时触发
compositionstart
事件 - 输入未结束且仍在输入中会触发
compositionupdate
事件 - 当我们按下回车或是选择了对应的文字插入到输入框时代表输入完成,此时触发
compositionend
事件
我们可以使用这几个方法实现表单验证,如我们希望中文输入完成后才验证其有效性而不是正在输入中就验证。
# 对象作为参数传递时,传递的是对象地址
下面🌰的输出结果是什么?
function changeObjProperty(o) {
o.siteUrl = "http://www.baidu.com"
o = new Object()
o.siteUrl = "http://www.google.com"
}
let webSite = new Object();
changeObjProperty(webSite);
console.log(webSite.siteUrl);
2
3
4
5
6
7
8
结果是:“http://www.baidu.com”
因为对象作为参数时,传递进去的是这个对象的引用地址。
function changeObjProperty(o) {
o.siteUrl = "http://www.baidu.com" // 改变对应地址内的对象属性值
o = new Object() // 变量 o 被指向新的地址
o.siteUrl = "http://www.google.com" // 变量 o 的改变与旧地址无关
}
let webSite = new Object();
changeObjProperty(webSite);
console.log(webSite.siteUrl);
2
3
4
5
6
7
8
# 关于函数的执行顺序
function Foo() {
Foo.a = function() {
console.log(1)
}
this.a = function() {
console.log(2)
}
}
// 以上只是 Foo 的构建方法,没有产生实例,此刻也没有执行
Foo.prototype.a = function() {
console.log(3)
}
// 现在在 Foo 上挂载了原型方法 a ,方法输出值为 3
Foo.a = function() {
console.log(4)
}
// 现在在 Foo 上挂载了直接方法 a ,输出值为 4
Foo.a();
// 立刻执行了 Foo 上的 a 方法,也就是刚刚定义的,所以
// # 输出 4
let obj = new Foo();
/* 这里调用了 Foo 的构建方法。Foo 的构建方法主要做了两件事:
1. 将全局的 Foo 上的直接方法 a 替换为一个输出 1 的方法。
2. 在新对象上挂载直接方法 a ,输出值为 2。
*/
obj.a();
// 因为有直接方法 a ,不需要去访问原型链,所以使用的是构建方法里所定义的 this.a,
// # 输出 2
Foo.a();
// 构建方法里已经替换了全局 Foo 上的 a 方法,所以
// # 输出 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
所以输出:4 2 1
# 操作符和类型转换
下面🌰的输出结果是什么?
1 + "1"
2 * "2"
[1, 2] + [2, 1]
"a" + + "b"
2
3
4
5
6
7
解析:
1 + "1"
// 加性操作符:如果只有一个操作数是字符串,则将另一个操作数转换为字符串,然后再将两个字符串拼接起来
// 所以值为:“11”
2 * "2"
// 乘性操作符:如果有一个操作数不是数值,则在后台调用 Number()将其转换为数值
// 所以值为:4
[1, 2] + [2, 1]
// Javascript中所有对象基本都是先调用valueOf方法,如果不是数值,再调用toString方法。
// 所以两个数组对象的toString方法相加,值为:"1,22,1"
"a" + + "b"
// 后边的“+”将作为一元操作符,如果操作数是字符串,将调用Number方法将该操作数转为数值,如果操作数无法转为数值,则为NaN。
// "a" + + "b"其实可以理解为
// + "b" -> NaN
// 所以值为:"aNaN"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Promise,宏任务和微任务
求下面代码的输出顺序和输出值,为什么?
var date = new Date()
console.log(1, new Date() - date)
setTimeout(() => {
console.log(2, new Date() - date)
}, 500)
Promise.resolve().then(console.log(3, new Date() - date))
while(new Date() - date < 1000) {}
console.log(4, new Date() - date)
2
3
4
5
6
7
8
9
10
11
12
13
执行结果为:
1 0
3 1
4 1000
2 1000
2
3
4
先执行同步任务,所以1先打印;
setTimeout
是宏任务,等待执行;
因为Promise.then()
的参数是一个表达式,不是一个函数且then
是立即执行的,所以打印3,随后将then
放入微任务中等待执行。如果then()
中是一个函数,则会等待同步任务执行完后执行;
while
循环是同步任务,会阻塞执行,在等待一秒后打印4;
此时同步任务执行完了,开始执行异步任务,先将then
取出来并执行,发现then的第一个参数是一个undefined
,promise
内部会判断,如果then
的第一个参数,也就是成功回调函数,不是一个参数的话,会自动给他包装成一个函数,并且将resolve
的value
值透传到下一个then里面。
最后去执行setTimeout
,打印2。
# 空数组使用map方法会如何?
原文链接:一道js笔试题, 刷新了我对map方法函数的认知,你做对了吗? (opens new window)
看一个🌰:
const array=new Array(5).map(item=>{
return item={
name:'1'
}
});
console.log(array) // 输出结果是什么?
2
3
4
5
6
正确答案是:
[empty x 5]
本以为会输出:
[{name: '1'}, {name: '1'}, {name: '1'}, {name: '1'}, {name: '1'}];
那么为什么会这样呢?
原因是它会去遍历每一项,并通过key in array来判断当前项是否为empty,如果是就不执行后续操作
V8的源码:
function ArrayMap(f, receiver) {
CHECK_OBJECT_COERCIBLE(this, "Array.prototype.map");
// Pull out the length so that modifications to the length in the
// loop will not affect the looping and side effects are visible.
var array = TO_OBJECT(this);
var length = TO_LENGTH(array.length);
if (!IS_CALLABLE(f)) throw %make_type_error(kCalledNonCallable, f);
var result = ArraySpeciesCreate(array, length);
for (var i = 0; i < length; i++) {
if (i in array) { // 在这里检查元素是否为空
var element = array[i];
%CreateDataProperty(result, i, %_Call(f, receiver, element, i, array));
}
}
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17