阿里巴巴前端笔试题:数组拍平

1

编写一个 JavaScript 函数,接受一个仅包含数字的 多维数组 ,返回拍平以后的结果。例如传入:[1, [[2], 3, 4], 5],返回 [1, 2, 3, 4, 5]

(本题来源:阿里巴巴前端笔试题)

1
2
3
4
5
6
7
8
9
10
11
const arr = [1, [[2], 3, 4], 5];

const flatten = arr => {
return arr.reduce((flat, toFlat) => {
return flat.concat(Array.isArray(toFlat) ? flatten(toFlat) : toFlat);
}, []);
};

const res = flatten(arr);

console.log(res);

2

编写一个 JavaScript generator 函数,接受一个仅包含数字的 多维数组 ,返回一个迭代器,可以遍历得到它拍平以后的结果。例如:

1
2
3
4
5
6
const numbers = flatten2([1, [[2], 3, 4], 5])
numbers.next().value // => 1
numbers.next().value // => 2
numbers.next().value // => 3
numbers.next().value // => 4
numbers.next().value // => 5

答案:

1
2
3
4
5
6
7
8
9
10
11
function* flatten2(a) {
var length = a.length;
for (var i = 0; i < length; i++) {
var item = a[i];
if (typeof item !== 'number') {
yield* flatten2(item);
} else {
yield item;
}
}
}

使用 Object.create(null) 代替大括号生成({})对象

传统对象写法为 var obj = {} 这种写法等价于 Object.create(Object.prototype) ,所以 {} 并不是真正的空对象,它还通过原型链继承了 Object 的属性方法。

这种方式不好地方是,{} 在某些时候会进行隐式类型转型,还有当我们使用 for...in 时候会遍历原形链上的属性方法,如下面的代码所示。

1
2
3
4
5
6
Object.prototype.test = 'test';
var obj = {};
console.log((obj + 0).length); // 16
for (var i in obj) {
console.log(i); // 输出 test
}

当对象和数值进行运算的时候会对对象进行隐式转换,{} + 0 就变成了 [object Object] + 0 所以计算结果的长度为 16。

for…in 进行遍历的时候也会遍历原型链可枚举的属性,所以当遍历的时候还需要使用 hasOwnProperty 进行过滤操作。

Object.create(null) 没有这样的问题,是一个没有继承 Object 原型的属性和方法的纯对象。

1
2
3
var obj = Object.create(null)
obj + obj // Uncaught TypeError: Cannot convert object to primitive value
obj + 1 // Uncaught TypeError: Cannot convert object to primitive value

性能方面,如下图,在以前使用 Object.create(null) 会有性能问题(比 {} 慢了很多倍),而现在已经不是问题了,而且更快了。

image

所以推荐大家使用 Object.create(null) 代替大括号生成({})对象。

为长数字添加逗号分隔符

第一个我想到的是使用正则来处理,不考虑小数的情况下:

1
2
3
function test(num) {
return String(num).replace(/(\d)(?=(\d{3})+$)/g, "$1,");
}

当然如果要考虑小数的话就得把数字先转化为字符串,然后 split 分割去第一部分即可。

1
2
3
4
5
6
7
8
9
10
11
function test(n) {
var n = n || 0;
n = (n + "")
.split(".")
.map(function(s, i) {
return i ? s : s.replace(/(\d)(?=(\d{3})+$)/g, "$1,");
})
.join(".");
return n;
}

其实还有一个更简单的方式,Node 下也可以使用:

1
2
3
function test(num) {
return num.toLocaleString('en-US')
}

另外在 i18n 的数字处理上已经有 API 了。

1
2
3
4
var number = 3500;

console.log(new Intl.NumberFormat().format(number));
// → '3,500' if in US English locale

而就处理速度而言,使用正则的方式更快一些,测试环境(Node 8.9.1 & Chrome 62) 。

image

JS 优化获取对象类型的方式

我们都知道可以通过 Object.prototype.toString.call(obj) 的方式来获取类型真实的类型。

不过我发现一个更简单而且更快的写法。利用大括号 {} 代替 Object.prototype, 在 #135 中我简单说明了, {} 等价于 Object.create(Object.prototype),所以这里就可以简写,没想到还变快了!

1
2
3
4
5
6
7
8
9
10
11
12
console.time("1");
Object.prototype.toString
.call(new Date())
.match(/\s(\w)/)[1]
.toLowerCase();
console.timeEnd("1");
console.time("2");
({}.toString
.call(new Date())
.match(/\s(\w)/)[1]
.toLowerCase());
console.timeEnd("2");

测试结果(node v8.9.1)

image

可以看到这种写法不仅简单,而且更快了。

写一个通用的 util 函数就是:

1
2
3
function getType (obj) {
return {}.toString.call(obj).match(/\s(\w+)/)[1].toLowerCase()
}

JS将字符串转为驼峰写法

就是干

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function toCamel(str) {
if (!str.indexOf('-') || typeof str !== 'string') throw new Error('the param is invalidation')
var strArr = str.split('-')
var _str = ''
strArr.forEach(function (v, k) {
if(k === 0){
_str += v.toLowerCase()
}else {
_str += (v[0].toUpperCase()+v.slice(1).toLowerCase())
}
})

return _str
}

var res = toCamel('Eric-is-a-gOoD-maN')
console.log(res)

使用正则

1
2
3
4
5
6
7
8
9
10
11
function toCamel(str) {
const reg = /(\w)-(\w)/g;
return str.replace(reg, (match, m1, m2) => {
console.log(match);
return m1 + m2.toUpperCase();
});
}

const str = "is-man-ma";

console.log(toCamel(str));

理解 Promise

语法

1
2
3
4
new Promise(
/* executor */
function(resolve, reject) {...}
);

参数

executor是一个带有resolve和reject两个参数的函数 。executor 函数在Promise构造函数执行时同步执行,被传递resolve和reject函数(executor 函数在Promise构造函数返回新建对象前被调用)。resolve 和 reject 函数被调用时,分别将promise的状态改为fulfilled(完成)或rejected(失败)。executor 内部通常会执行一些异步操作,一旦完成,可以调用resolve函数来将promise状态改成fulfilled,或者在发生错误时将它的状态改为rejected。如果在executor函数中抛出一个错误,那么该promise 状态为rejected。executor函数的返回值被忽略。

Promise 对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers )。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象

一个 Promise有以下几种状态:

  • pending: 初始状态,不是成功或失败状态。
  • fulfilled: 意味着操作成功完成。
  • rejected: 意味着操作失败。

pending 状态的 Promise 对象可能触发fulfilled 状态并传递一个值给相应的状态处理方法,也可能触发失败状态(rejected)并传递失败信息。当其中任一种情况出现时,Promise 对象的 then 方法绑定的处理方法(handlers )就会被调用(then方法包含两个参数:onfulfilled 和 onrejected,它们都是 Function 类型。当Promise状态为fulfilled时,调用 then 的 onfulfilled 方法,当Promise状态为rejected时,调用 then 的 onrejected 方法, 所以在异步操作的完成和绑定处理方法之间不存在竞争)。

因为 Promise.prototype.then 和 Promise.prototype.catch 方法返回promise 对象, 所以它们可以被链式调用。

Promise.then()会始终接收上一层Promise的返回值,使用 resolve(value) 或者 return value ,注意:在JS中无返回值的函数是返回的underfined

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

// 有返回值,并且为1
function func1(){
return 1;
}

// 无返回值
function func2(){
console.log(2)
}

//无返回值
function func3(){
func1();
}

Promise.then(),接收一个参数,如果参数为函数才会上一个then与异步(即等待上一个Promise执行结束后再执行),如果参数返回的是Promise对象才会下一个then异步进行(即当前执行结束后才会执行下一个then)。

在这些例子中,我假定 doSomething() 和 doSomethingElse() 均返回 promises,并且这些 promises 代表某些在 JavaScript event loop (如 IndexedDB, network, setTimeout) 之外的某些工作结束,这也是为何它们在某些时候表现起来像是并行执行的意义。这里是一个模拟用的 JSBin

1
2
3
doSomething().then(function () {
return doSomethingElse();
}).then(finalHandler);

Answer:

1
2
3
4
5
6
doSomething
|-----------------|
doSomethingElse(undefined)
|------------------|
finalHandler(resultOfDoSomethingElse)
|------------------|

Puzzle2

1
2
3
doSomething().then(function () {
doSomethingElse();
}).then(finalHandler);

Answer:

1
2
3
4
5
6
doSomething
|-----------------|
doSomethingElse(undefined)
|------------------|
finalHandler(undefined)
|------------------|

Puzzle3

1
2
doSomething().then(doSomethingElse())
.then(finalHandler);

Answer:

1
2
3
4
5
6
doSomething
|-----------------|
doSomethingElse(undefined)
|---------------------------------|
finalHandler(resultOfDoSomething)
|------------------|

Puzzle4

1
2
doSomething().then(doSomethingElse)
.then(finalHandler);

Answer:

1
2
3
4
5
6
doSomething
|-----------------|
doSomethingElse(resultOfDoSomething)
|------------------|
finalHandler(resultOfDoSomethingElse)
|------------------|

补充:看其它的文章,不如看看规范,而且不长,https://promisesaplus.com/

js:找出字符串中重复数量最多的字母

1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var str = 'cbdefdfpoweqwlkjsaaaaz'
function findMostLetter(str) {
// 去除空格并排序
let match = str
.replace(" ", "")
.split("")
.sort()
.join("")
.match(/(\w)\1*/g);
let res = "";
let len = 0;
match.forEach((val, key) => {
if (val.length > len) {
res = val[0];
len = val.length;
}
});
return res;
}

findMostLetter(str)

2

1
2
3
4
5
6
7
8
9
10
11
12
13
function test(str) {
return str
.replace(" ", "")
.split("")
.sort()
.join("")
.match(/(\w)\1*/g)
.sort((x, y) => x.length - y.length)
.pop()[0];
}

const res = test("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa dddddddfdf");
console.log(res);

js生成随机字符串以及任意范围的数字

代码细节

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
function radom_num(min, max) {
// 如果非数字则报错
if (typeof min !== 'number' || typeof max !== 'number')
throw new Error('param invalidate,should be a number')
// 如果顺序有问题则互换位置
if (min > max) {
[min, max] = [max, min]
}
// 如果二者相等则返回一个
if (min === max) {
return min
}
return Math.floor(Math.random() * (max - min + 1)) + min
}

function ramdom_str(length = 6) {
var str = 'abcdefghijklmnopqrstuvwxyz'
str += str.toUpperCase();
str += '0123456789'
var _str = ''
for (let i = 0; i < length; i++) {
var rand = Math.floor(Math.random() * str.length)
_str += str[rand];
}

return _str
}

另外,我是用 TypeScript 写了兼容浏览器端和服务器端的随机数获取库,直接使用npm i -S random.ts 安装即可,如下是使用方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ES6
import { getNum, getStr, getSafer } from 'random.ts'
import * as Random from 'random.ts'
// Node
var Random = require('random.ts')

// get random number of given range
// this is equal with getNum(100, 2)
Random.getNum(2, 100)

// if param is not number type always return 0
Random.getNum("string", "string")

// if param out of safe range always return 0
Random.getNum(0, 2 ** 53)

// get random string,default length is 6
Random.getStr(6)

// get safer random string, default length is 16
// v1.3.0 only supports node.js
// after v1.3.0 supports ES6 browser(using ArrayBuffer and window.crypto)
// for better compatibility in favor of getStr()
Random.getSafer()

js巧用乘法进行小数处理和Unicode字符串快速转换成中文

小技巧:Unicode字符串快速转换成中文

1
2
3
4
5
6
7
8
function conversion(str) {
var json = `{"v":"${str}"}`
return JSON.parse(json).v
}

var res = conversion('\u6211\u662f\u0075\u006e\u0069\u0063\u006f\u0064\u0065')

console.log(res)

补充内容:

每个字符在JavaScript内部都是以16位(即2个字节)的UTF-16格式储存。也就是说,JavaScript的单位字符长度固定为16位长度,即2个字节。

但是,UTF-16有两种长度:对于U+0000到U+FFFF之间的字符,长度为16位(即2个字节);对于U+10000到U+10FFFF之间的字符,长度为32位(即4个字节),而且前两个字节在0xD800到0xDBFF之间,后两个字节在0xDC00到0xDFFF之间。举例来说,U+1D306对应的字符为𝌆,它写成UTF-16就是0xD834 0xDF06。浏览器会正确将这四个字节识别为一个字符,但是JavaScript内部的字符长度总是固定为16位,会把这四个字节视为两个字符。

1
2
3
4
5
6
7
8
9
var s = '\uD834\uDF06';

s // "𝌆"
s.length // 2
/^.$/.test(s) // false
s.charAt(0) // ""
s.charAt(1) // ""
s.charCodeAt(0) // 55348
s.charCodeAt(1) // 57094

上面代码说明,对于于U+10000到U+10FFFF之间的字符,JavaScript总是视为两个字符(字符的length属性为2),用来匹配单个字符的正则表达式会失败(JavaScript认为这里不止一个字符),charAt方法无法返回单个字符,charCodeAt方法返回每个字节对应的十进制值。

es6增加了对这类字符的处理:http://es6.ruanyifeng.com/#docs/string

巧用乘法进行小数处理

题1:
一个商城要对购物车总价进行处理,比如说价格区间为 [1.00, 1.02] 返回 1.00;[1.03, 1.07] 返回 1.05;[1.08, 1.09] 返回 1.10。

方法来自: https://segmentfault.com/q/1010000011578508

1
2
3
function format(price) {
return return Math.round(elem*20)/20;
}

题2:
商城中商品评分用图形星星表示,但是满分为5星(以后可能回更改10分制),可以为半星,不足5分的地方用灰色的星星表示,当评分区间[1,1.4]的时候,直接渲染1个星星;评分为[1.5,1.9],直接渲染1.5个星星。

方法来自:https://github.com/ustbhuangyi/vue-sell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const LENGTH = 5;
const CLS_ON = 'on';
const CLS_HALF = 'half';
const CLS_OFF = 'off';
function(score) {
let result = [];
let score = Math.floor(score * 2) / 2;
let hasDecimal = score % 1 !== 0;
let integer = Math.floor(score);
for (let i = 0; i < integer; i++) {
result.push(CLS_ON);
}
if (hasDecimal) {
result.push(CLS_HALF);
}
while (result.length < LENGTH) {
result.push(CLS_OFF);
}
return result;
}

给 Array.prototype.reduce() 加一个初始值

Array.prototype.reduce() 方法很简单,直接加一个回调函数即可。

1
2
3
const tmp = [0, 1, 2, 3].reduce((sum, elem) => sum + elem)
console.log(tmp)
// 输出 6

再进阶一点,callback 可以传入 4 个参数

  • Accumulator (acc) (累计器)
  • Current Value (cur) (当前值)
  • Current Index (idx) (当前索引,可选)
  • Source Array (src) (源数组,可选)

callback 函数的返回值分配给累计器,其值在数组的每个迭代中被记住,并最后成为最终的单个结果值。引用自 MDN

当然再进阶一些,除了可以传入 callback 还可以再传入一个初始值,改一下上面的例子就是下面这样

1
2
3
const tmp = [0, 1, 2, 3].reduce((sum, elem) => sum + elem, 100)
console.log(tmp)
// 输出 106

如果没有提供初始值,则将使用数组中的第一个元素。

不过一定要注意,如果在没有初始值的空数组上调用 reduce 将报错!

1
2
[].reduce((a, b) => a + b)
// TypeError: Reduce of empty array with no initial value

这点在从客户端传入服务端的内容是需要特别注意的,反之亦然,所以最佳实践是给 Array.prototype.reduce() 方法第二个参数的加一个返回值类型的初始值。

1
2
3
const BigNumber = require("bignumber.js");
// 需要返回一个 BigNumber 类型的参数
const result = [0.1, 0.2, 0.3].reduce((sum, next) => sum.plus(next), new BigNumber(0));