数组是 JS 中使用频率仅次于对象的数据结构,官方提供了众多的 API。
今天我们来谈谈如何扁平化(flatten)数组。
扁平化就是将嵌套的数组变成一维数组的过程。
通常有几种方法可以实现扁平化:
第一个就是迭代递归法
var array = [[1,2,3],4,5,6,[[7]],[]]
var result = flatten(array)
console.log(result)
for…of 实现
function flatten(arr, result = []) {
for (let item of arr) {
if (Array.isArray(item))
flatten(item, result)
else
result.push(item)
}
return result
}
迭代器实现:
众所周知,数组在 JS 中是一种可迭代结构,所以我们可以利用这一点修改它的迭代器实现扁平化:
Array.prototype[Symbol.iterator] = function() {
let arr = [].concat(this)
const getFirst = function(array) {
let first = array[0]
// 去掉为 [] 的元素
while (Array.isArray(array[0]) && array.length === 0) {
array.shift()
}
if (Array.isArray(first)) {
// 即将是 []
if (first.length === 1) array.shift()
return getFirst(first)
} else {
array.shift()
return first
}
}
return {
next: function() {
let item = getFirst(arr)
if (item) {
return {
value: item,
done: false,
}
} else {
return {
done: true,
}
}
},
}
}
生成器:
迭代器的升级版就是生成器(Generator),其实这种扁平化最适合用生成器来做了,因为我们的目的就是生成一个个的值,然后把它们组织成一维数组:
function* flat(arr) {
for (let item of arr) {
if (Array.isArray(item))
yield* flat(item)
else
yield item
}
}
function flatten(arr) {
let result = []
for (let val of flat(arr)) {
result.push(val)
}
return result
}
reduce 三句实现法
function flatten(arr) {
return arr.reduce((flat, toFlatten) => {
return flat.concat(Array.isArray(toFlatten) ? flatten(toFlatten) : toFlatten);
}, []);
}
reduce 是函数式编程两大法宝之一,中文翻译为化简,用它来实现,简直是巧妙。
第二个曲线救国法:
function flatten(arr){
let str = arr.toString()
return str.split(',')
}
管你原来是几维,先来个二向箔:转成字符串,之后再复原成数组,不过这个方法有个缺点,就是原来的空数组转的空字符串也会被放入新生成的数组里去。所以如果不需要空串元素的话还需要对结果进行过滤操作。
除了直接调用它的 toString 方法之外,还可以用隐式转换间接调用:
function flatten(arr){
return (arr + '').split(',')
}
写到最后:
Array.prototype.flatten 递归地将数组展按照指定的 depth 进行展平,depth 的默认值为 1。
// Flatten one level:
const array = [1, [2, [3]]];
array.flatten();
// → [1, 2, [3]]
// Flatten recursively until the array contains no more nested arrays:
array.flatten(Infinity);
// → [1, 2, 3]
同样的提议还包括 Array.prototype.flatMap,如同 Array.prototype.map 一样,可以在参数里面传递一个回调函数。
[2, 3, 4].flatMap((x) => [x, x * 2]);
// → [2, 4, 3, 6, 4, 8]
斐波那契数列又被称为黄金分割数列。
指的是这样的一个数列:1,1,2,3,5,8,13,21,34…,它有如下递推的方法
定义:F(1)=1,F(2)=1,F(n)=F(n-1)+F(n-2)(n>=2,n是正整数).
请使用js实现斐波那契函数:
第一种方法:
function fibonacci(n){
if(n < 0) throw new Error('输入的数字不能小于0');
if(n==1 || n==2){
return 1;
}else{
return fibonacci1(n-1) + fibonacci1(n-2);
}
}
优点:比较简洁易懂;
缺点:当数字太大时,会变得特别慢,要计算之前的数值。
所以:使用普通的递归,会造成不必要的浪费,所以我们首先想到的应该是将每次产生的递归值保存下来,下次直接使用就行,代码如下:
function fibonacci(n){
if(n < 0) throw new Error('输入的数字不能小于0');
let arr = [0,1];//在外部函数中定义数组,内部函数给数组添加值
function calc(n){
if(n<2){
return arr[n];
}
if(arr[n] != undefined){
return arr[n];
}
let data = calc(n-1) + calc(n-2);//使用data将每次递归得到的值保存起来
arr[n] = data;//将每次递归得到的值放到数组中保存
return data;
}
return calc(n);
}
直接使用数组实现(动态规划)
和方法2的思想类似,为了避免后续的重复计算,需要将计算过的值保存起来,我们可以直接使用数组进行保存。
function fibonacci(n){
var a = [0,1,1];
if(n < 0) throw new Error('输入的数字不能小于0');
if(n >= 3){
for(var i=3;i<=n;i++){
a[i] = a[i-1]+a[i-2];
}
}
return a[n];
}
直接使用变量实现
相校于使用数组的方式去存放,使用变量的方式就不会那么浪费内存了,因为总共只会有3个变量,但是也有缺点,它只能保存最后计算的值以及前两个值,以前的值会被替换掉。
function fibonacci(n){
var pre = 0;//表示前一个值
var cur = 1;//表示后一个值
var data;//表示当前值
if(n < 0) throw new Error('请输入大于0的值!');
if(n == 0) return 0;
if(n == 1) return 1;
if(n > 2){
for(var i=2;i<=n;i++){
data = pre + cur;
pre = cur;
cur = data;
}
}
return data;
}
回文就是:根据中心左右对称的字符串;
如:php aaccaa FFGGFF等,简单的说就是正读和反读都一样。
原理:定义一个方法,我们把判断的字符串传进去。
第一先判断他是否是string格式,
是的话 我们把它塞进一个数组里(split())
然后倒叙排列(reverse())
最后拆分为字符串(join())
相反就直接return false;
function test(abc){
// typeof js中判断一个变量的类型
// split 把一个字符串 分割成字符串数组
// reverse 颠倒数组中的顺序
// join 把一个数组塞进一个字符串
if(typeof abc == 'string')
return abc.split('').reverse('').join('') == abc;
return false;
}
console.log(test('php')+' '+test('script'))
function sortNumber(a,b){
return a-b;//升序
//return b-a;//降序
}
//js实现随机选取10–100之间的10个数字,存入一个数组,并排序
var iArray =[];
function getRandom(iStart,iEnd){
var iChoice = iStart-iEnd+1;
return Math.abs(Math.floor(Math.random()*iChoice))+iStart;
}
for(var i=0;i<10;i++){
iArray.push(getRandom(10,100))
}
iArray.sort(sortNumber);
console.log(iArray)
原生JS。
使用Date(方法)获取日期。
var myDate=new Date() ;
console.log(myDate);
console.log(Date());
alert(Date());
解法 1:动态规划
定义状态数组dp[i]的含义:数组中元素下标为[0, i]的连续子数组最大和。
状态转移的过程如下:
初始情况:dp[0] = nums[0]
若 nums[i] > 0,那么 dp[i] = nums[i] + dp[i - 1]
若 nums[i] <= 0,那么 dp[i] = nums[i]
var maxSubArray = function(nums) {
const dp = [];
let res = (dp[0] = nums[0]);
for (let i = 1; i < nums.length; ++i) {
dp[i] = nums[i];
if (dp[i - 1] > 0) {
dp[i] += dp[i - 1];
}
res = Math.max(res, dp[i]);
}
return res;
};
o(n)&0(n)
解法 2:原地进行动态规划
在解法 1 中开辟了 dp 数组。其实在原数组上做修改,用nums[i]来表示dp[i]。所以解法 1 的代码可以优化为:
var maxSubArray = function(nums) {
let res = nums[0];
for (let i = 1; i < nums.length; ++i) {
if (nums[i - 1] > 0) {
nums[i] += nums[i - 1];
}
res = Math.max(res, nums[i]);
}
return res;
};
0(n)&0(1)
解法 3:贪心法
本题的贪心法的思路是:在循环中找到不断找到当前最优的和 sum。
注意:sum 是 nums[i] 和 sum + nums[i]中最大的值。
这种做法保证了 sum 是一直是针对连续数组算和。
var maxSubArray = function(nums) {
let maxSum = (sum = nums[0]);
for (let i = 1; i < nums.length; ++i) {
sum = Math.max(nums[i], sum + nums[i]);
maxSum = Math.max(maxSum, sum);
}
return maxSum;
};
0(n)&0(1)
这个用正则的方式,比较简单。
w+ : w 表示 匹配任何字类字符,包括下划线。与"[A-Za-z0-9_]"等效。 + 表示:一次或多次匹配前面的字符或子表达式。
例如:"zo+"与"zo"和"zoo"匹配,但与"z"不匹配。+ 等效于 {1,}。
\1+ 表示:重复问上面捕获组里的内容一次或多次
function repeatedSubstringPattern (str) {
return /^(\w+)\1+$/.test(str)
}
const result4 = repeatedSubstringPattern('abcabc')
// true
数组转树形结构:
var data = [
{"id":2,"name":"第一级1","pid":0},
{"id":3,"name":"第二级1","pid":2},
{"id":5,"name":"第三级1","pid":4},
{"id":100,"name":"第三级2","pid":3},
{"id":6,"name":"第三级2","pid":3},
{"id":601,"name":"第三级2","pid":6},
{"id":602,"name":"第三级2","pid":6},
{"id":603,"name":"第三级2","pid":6}
];
数组转树形结构数据
(原理即为通过设置id为key值,再通过pid去找这个key是否一样,一样则为这数据的子级数据)
function arrayToJson(treeArray){
var r = [];
var tmpMap ={};
for (var i=0, l=treeArray.length; i
使用场景;在 js 生成验证码或者随机选中一个选项时很有用。
//生成从minNum到maxNum的随机数
function randomNum(minNum,maxNum){
switch(arguments.length){
case 1:
return parseInt(Math.random()*minNum+1,10);
break;
case 2:
return parseInt(Math.random()*(maxNum-minNum+1)+minNum,10);
break;
default:
return 0;
break;
}
}
我们有一个需求,用js 实现一个无限极累加的函数.
形如 add(1) //=> 1;
add(1)(2) //=> 2;
add(1)(2)(3) //=> 6;
add(1)(2)(3)(4) //=> 10; 以此类推.
补充一点知识:
函数柯里化(curry)是函数式编程里面的概念。curry的概念很简单:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
简单点来说就是:每次调用函数时,它只接受一部分参数,并返回一个函数,直到传递所有参数为止。
const add = (x, y) => x + y;
add(1, 2);
改成每次只接受一个参数的函数
const add = x => y => x + y;
add(1)(2);
柯里化主要有3个作用: 参数复用、提前返回和 延迟执行。
主要思路:要判断当前传入函数的参数个数 (args.length) 是否大于等于原函数所需参数个数 (fn.length) 。
如果是,则执行当前函数;如果是小于,则返回一个函数。
const curry = (fn, ...args) =>
// 函数的参数个数可以直接通过函数数的.length属性来访问
args.length >= fn.length // 这个判断很关键!!!
// 传入的参数大于等于原始函数fn的参数个数,则直接执行该函数
? fn(...args)
/**
* 传入的参数小于原始函数fn的参数个数时
* 则继续对当前函数进行柯里化,返回一个接受所有参数(当前参数和剩余参数) 的函数
*/
: (..._args) => curry(fn, ...args, ..._args);
function add1(x, y, z) {
return x + y + z;
}
const add = curry(add1);
console.log(add(1, 2, 3));
console.log(add(1)(2)(3));
console.log(add(1, 2)(3));
console.log(add(1)(2, 3));
Ramda
Ramda 中的函数所有都支持柯里化。也就是说,所有的多参数函数,默认都可以使用单参数函数。
还是举上面的例子
const addThreeNumbers = (x, y, z) => x + y + z;
const curriedAddaddThreeNumbers = R.curry(addThreeNumbers);
const f = curriedAddaddThreeNumbers(1, 2);
console.log(f(3));
大名鼎鼎的 lodash 中也提供了 柯里化 函数 ,那么它和Ramda有什么区别呢
lodash是一个很强大的工具函数库,比如 节流,防抖,深拷贝等等,只要引入 lodash ,我们就可以直接使用。
Ramda 是一个函数式编程风格的函数库。
我们来简单的解释一下:
参数复用:拿上面 f这个函数举例,只要传入一个参数 z,执行,计算结果就是 1 + 2 + z 的结果,1 和 2 这两个参数就直接可以复用了。
提前返回 和 延迟执行 也很好理解,因为每次调用函数时,它只接受一部分参数,并返回一个函数(提前返回),直到(延迟执行)传递所有参数为止。