对象的值翻倍(考点对象与数组的转换)
有一个带有价格的 prices
对象。
- 编写函数
doublePrices (prices)
,使单价翻倍 - prices对象有几个属性,忽略symbol属性,只计算常规属性
let prices = {
banana: 1,
orange: 2,
meat: 4,
};
function doublePrices(prices) {
// 把对象转为数组
let priceArr = Object.entries(prices); // [["banana", 1], ["orange", 2], ["meat", 4]]
// 注意:priceArr是一个二维数组
// 处理单价
priceArr.map(item => [item[0], item[1] * 2])
// 把处理好价格的数组转为对象并返回
return Object.fromEntries(priceArr)
}
doublePrices(prices);
// 使用Object.keys() / Object.values()获取key/value数组
let length = Object.keys(prices).length
计算最高单价
新建一个函数 topPrice(prices)
,返回单价最高的key。
let prices = {
banana: 1,
orange: 2,
meat: 4,
};
function topPrice(prices) {
let maxPrice = 0
let maxName = null
for(let [key, value] of Object.entries(prices)){
if(maxPrice < value) {
maxPrice = value
maxName = key
}
}
return maxName
}
排除反向引用(考查JSON.stringify(value[, replacer, space]))
知识点:
语法:
let str = JSON.stringify(value[, replacer, space])
属性说明:
value
,要编码的值。replacer
,要编码的属性数组或映射函数 function(key, value)。
请注意replacer
函数会获取每个键/值对,包括嵌套对象和数组项。space
,用于格式化的空格数量。
题:假如有2个对象room
和 meetup
,他们互相引用对方。编写 replacer
函数,移除引用 meetup
的属性,并将其他所有属性序列化。
let room = {
number: 23
};
let meetup = {
title: "Conference",
occupiedBy: [{name: "John"}, {name: "Alice"}],
place: room
};
// room 和 meetup循环引用
room.occupiedBy = meetup;
meetup.self = meetup;
console.log(room)
console.log(meetup)
// 直接转换会报错
// JSON.stringify(meetup); // Error: Converting circular structure to JSON
let newMeetup = JSON.stringify(meetup, function replacer(key, value){
console.log(key, value)
// 判断 key !="" 以排除第一个调用时 value 是 meetup 的情况。
return (key != "" && value == meetup) ? undefined : value;
})
注意:为什么要判断key != ""
?
查看console.log(key, value)
打印结果,可以看到打印的第一是":[object Object]"
。
因为replacer
的第一个调用很特别。它是使用特殊的“包装对象”制作的:{"": meetup}
。换句话说,第一个 (key, value)
对的键是空的,并且该值是整个目标对象。
对数字求和到给定值(递归)
编写一个函数 sumTo(n)
计算 1 + 2 + … + n 的和。
用三种方式实现:
- 使用循环。
- 使用递归,对
n > 1
执行sumTo(n) = n + sumTo(n-1)
。 - 使用 等差数列 求和公式:
sumTo(n) = n*(n+1)/2
使用循环
function sumTo(n) {
let sum = 0;
for (let i = 1; i <= n; i++) {
sum += i;
}
return sum;
}
console.log( sumTo(100) ); // 5050
使用递归
function sumTo(n) {
if (n == 1) return 1;
return n + sumTo(n - 1);
}
console.log( sumTo(100) ); // 5050
使用等差数列
function sumTo(n) {
return n * (n + 1) / 2;
}
console.log( sumTo(100) );
- 哪种解决方式最快?哪种最慢?为什么?
当然是公式解法最快。对任何数字 n,只需要进行 3 次运算。数学大法好!
循环的速度次之。在循环和递归方法里,我们对相同的数字求和。但是递归涉及嵌套调用和执行堆栈管理,占用资源,因此递归的速度更慢一些。
- 我们可以使用递归来计算 sumTo(100000) 吗?
一些引擎支持“尾调用(tail call)”优化:如果递归调用是函数中的最后一个调用(例如上面的 sumTo),那么外部的函数就不再需要恢复执行,因此引擎也就不再需要记住他的执行上下文。这样就减轻了内存负担,因此计算 sumTo(100000) 就变得可能。但是如果你的 JavaScript 引擎不支持尾调用优化,那就会报错:超出最大堆栈深度,因为通常总堆栈的大小是有限制的。
计算阶乘(递归)
阶乘定义:
阶乘是一个数学术语。它表示一个正整数的阶乘是所有小于及等于该数的正整数的积。具体来说,一个自然数n的阶乘写作n!
。例如,5的阶乘(写作5!)就是1×2×3×4×5=120。阶乘的计算可以通过递归方式进行,其中0的阶乘定义为1(0! = 1)。
n! = n * (n - 1) * (n - 2) * ...*1
编写一个函数 factorial(n)
使用递归调用计算 n!
。
根据定义,阶乘 n!
可以被写成 n * (n-1)!
。
换句话说,factorial(n)
的结果可以用 n
乘以 factorial(n-1)
的结果来获得。对 n-1
的调用同理可以依次地递减,直到 1
。
function factorial(n) {
return (n != 1) ? n * factorial(n - 1) : 1;
}
console.log( factorial(5) ); // 120
递归的基础是数值 1
。我们也可以用 0
作为基础,不影响,除了会多一次递归步骤:
function factorial(n) {
return n ? n * factorial(n - 1) : 1;
}
console.log( factorial(5) ); // 120
输出一个单链表(递归、循环)
编写一个可以逐个输出链表元素的函数 printList(list)
。
使用两种方式实现:循环和递归。
// 单链表
let list = {
value: 1,
next: {
value: 2,
next: {
value: 3,
next: {
value: 4,
next: null
}
}
}
};
使用循环
function printList(list) {
// 使用变量tmp,是为了方便扩展,使用链表做其它工作
let tmp = list;
while (tmp) {
alert(tmp.value);
tmp = tmp.next;
}
}
printList(list);
使用递归
function printList(list) {
// 使用变量tmp,是为了方便扩展,使用链表做其它工作
let tmp = list;
console.log(tmp.value)
if(tmp.next) printList(tmp.next)
}
printList(list);
反向输出单链表(递归、循环)
刚刚的任务是从上到下输出单链表list。
现在要求编写一个反向输出单链表的函数printReverseList(list)
。
使用两种解法:循环和递归。
// 单链表
let list = {
value: 1,
next: {
value: 2,
next: {
value: 3,
next: {
value: 4,
next: null
}
}
}
};
使用循环
function printReverseList(list) {
let arr = [];
let tmp = list;
while (tmp) {
arr.push(tmp.value);
tmp = tmp.next;
}
for (let i = arr.length - 1; i >= 0; i--) {
alert( arr[i] );
}
}
printReverseList(list);
使用递归
function printReverseList(list) {
// 使用变量tmp,是为了方便扩展,使用链表做其它工作
let tmp = list;
if(tmp.next) printReverseList(tmp.next)
// 递归结束后,才开始执行此语句,读取存储在堆栈中的数据
console.log(tmp.value)
}
printReverseList(list);
递归在执行的时候,它顺着链表,记录每一个嵌套调用里链表的元素(在执行上下文堆栈里),当next=null
,递归函数执行结束,从堆栈中恢复之前的执行上下文,并从停止的位置恢复外部函数。
// 存储在 执行上下文堆栈 的特殊数据结构 中的执行上下文
Context: { list: { value: 4, next: null } }
Context: { list: { value: 3, next: {...} } }
Context: { list: { value: 2, next: {...} } }
Context: { list: { value: 1, next: {...} } }
console.log(tmp.value)
语句,从上往下依次输出存储在 执行上下文堆栈 中的数据。
斐波那契数(循环、递归)
编写一个函数 fib(n)
返回第 n
个斐波那契数。
斐波那契数列是指这样一个数列:1,1,2,3,5,8,13,21,34,55,89……
这个数列从第3项开始 ,每一项都等于前两项之和。
在数学上,斐波那契数列以如下递推的方法定义:F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*)。
使用循环
推理:
// 根据定义,fib(1) = 1, fib(2) = 1
let a = 1, b = 1
// 从第3项开始,每一项都是前两项的和。因此 fib(3) = 1 + 1 = 2
let c = a + b;
/* 现在我们有 a = fib(1), b = fib(2), c = fib(3)
a b c
1, 1, 2
*/
// 现在我们想要得到 fib(4) = fib(2) + fib(3)。
// 移动变量:a,b 将得到 fib(2),fib(3),c 将得到两者的和:
/* a = fib(2) = 1, b = fib(3) = 2, c = fib(4) = a + b = 3
a b c
1, 1, 2, 3
*/
具体实现:
function fib(n) {
let a = 1;
let b = 1;
// 从第3项开始,每一项都等于前两项之和。
for (let i = 3; i <= n; i++) {
let c = a + b;
a = b;
b = c;
}
return b;
}
console.log( fib(3) ); // 2
console.log( fib(7) ); // 13
使用递归
斐波那契数根据定义是递归的:
function fib(n) {
return n <= 1 ? n : fib(n - 1) + fib(n - 2);
}
console.log( fib(3) ); // 2
console.log( fib(5) ); // 5
因为函数产生了太多的子调用。同样的值被一遍又一遍地计算。使用递归会很占内存。
以fib(5)
为例:
fib(5) = fib(4) + fib(3)
fib(4) = fib(3) + fib(2)
fib(3) = fib(2) + fib(2)
从上面可以看出,fib(5)
计算1次,fib(4)
计算1次,fib(5)
、fib(4)
都需要fib(3)
,fib(3)
被独立计算了2次。计算量远超于n
。
循环可以重复使用已经计算过的数,递归每次都要独立计算,因此斐波那契数列不建议使用递归,太占内存了,比循环慢。
实现函数sum(a)(b) = a + b。(JavaScript闭包)
在JavaScript中,你可以通过闭包来实现这样的sum(a)(b)
函数,它允许你链式调用,实现两个数值的相加。
function sum(a) {
return function(b) {
return a + b; // 从外部词法环境获得 "a"
};
}
console.log( sum(1)(2) ); // 3
sum
函数接受一个参数 a
,并返回一个接受一个参数 b
的新函数。当你调用 sum(a)(b)
时,内部的函数会被执行,并返回 a + b
的结果。
通过函数筛选数组
我们有一个内建的数组方法 arr.filter(f)
。它通过函数 f
过滤元素。如果它返回 true
,那么该元素会被返回到结果数组中。
制造一系列“即用型”过滤器:
- inBetween(a, b) —— 在 a 和 b 之间或与它们相等(包括)。
- inArray([…]) —— 包含在给定的数组中。
用法如下所示:
- arr.filter(inBetween(3,6)) —— 只挑选范围在 3 到 6 的值。
- arr.filter(inArray([1,2,3])) —— 只挑选与 [1,2,3] 中的元素匹配的元素。
function inBetween(a, b) {
return function(x) {
return x >= a && x <= b;
};
}
function inArray(arr) {
return function(x) {
return arr.includes(x);
};
}
let arr = [1, 2, 3, 4, 5, 6, 7];
console.log( arr.filter(inBetween(3, 6)) ); // 3,4,5,6
console.log( arr.filter(inArray([1, 2, 10])) ); // 1,2
每秒输出1次(setInterval/setTimeout)
编写一个函数 printNumbers(from, to)
,使其每秒输出一个数字,数字从 from
开始,到 to
结束。
- 使用
setInterval
。
function printNumbers(from, to) {
let current = from;
let timerId = setInterval(function() {
console.log(current);
if (current == to) {
clearInterval(timerId);
}
current++;
}, 1000);
}
printNumbers(5, 10);
- 使用嵌套的
setTimeout
。
function printNumbers(from, to) {
let current = from;
let timerId = setTimeout(go, 1000)
function go() {
console.log(current);
if(current < to) {
setTimeout(go, 1000)
}
current++
}
}
间谍装饰器
创建一个装饰器 spy(func)
,它应该返回一个包装器,该包装器将所有对函数的调用保存在其 calls 属性中。
每个调用都保存为一个参数数组。
function work(a, b) {
console.log( a + b ); // work 是一个任意的函数或方法
}
// spy(func)对传入的函数 func 进行包装和监视
function spy(func) {
// 包装器函数wrapper
function wrapper(...args) {
console.log('参数:', args)
// 使用...args 替代 arguments 来将参数存储为"真实"的数组在 wrapper.calls 中
wrapper.calls.push(args);
console.log(wrapper.calls)
// 调用原始函数func
return func.apply(this, args);
}
wrapper.calls = [];
return wrapper;
}
let spyWork = spy(work);
spyWork(1, 2); // 3
spyWork(4, 5); // 9
for (let args of spyWork.calls) {
console.log( 'call:' + args.join() ); // "call:1,2", "call:4,5"
}
延时装饰器
创建一个装饰器 delay(f, ms)
,该装饰器将 printFn
的每次调用延时 ms
毫秒。
function printFn(x) {
console.log(x);
}
function delay(func, ms) {
return function() {
setTimeout(() => {
func.apply(this, arguments)
}, ms)
}
}
// 创建包装器
let printFn1000 = delay(printFn, 1000);
let printFn2000 = delay(printFn, 2000);
printFn1000("hello"); // 在 1000ms 后显示 "hello"
printFn2000("world"); // 在 2000ms 后显示 "world"
为什么定时器使用箭头函数?
箭头函数没有自己的 this
和 arguments
,所以 func.apply(this, arguments)
从包装器中获取 this
和 arguments
。
防抖装饰器(函数防抖)
创建一个防抖装饰器debounce(func, ms)
。
如果一个事件被频繁执行多次,并且触发的时间间隔过短,则防抖函数可以使得对应的事件处理函数,只执行最后触发的一次。函数防抖可以把多个顺序的调用合并成一次。
实现原理:
在事件被触发ms
毫秒后再执行回调,如果在这ms
毫秒内又被触发,则重新计时;典型的案例就是输入搜索:输入结束后ms
毫秒才进行搜索请求,ms
毫秒内又输入的内容,就重新计时。
function f(x) {
console.log(x);
}
function debounce(func, ms) {
let timeId;
return function() {
clearTimeout(timeId);
timeId = setTimeout(() => {
func.apply(this, arguments)
}, ms)
}
}
// 创建包装器
let showInput = debounce(f, 1000);
// 假如这是一个input框的输入事件
handleInput(value) {
showInput("hello"); // 在 1000ms 后显示 "hello"
}
在这个1000毫秒的时间里,用户不断的输入,定时器会不断的被清除再重新设置一个。它的内容是最后一次输入的 1000ms 后被处理的。
节流装饰器(函数节流)
创建一个“节流”装饰器 throttle(f, ms)
。
函数节流(throttle)是一种用于控制函数执行频率的技术。它的主要目的是限制某个函数在一定时间间隔内只能执行一次,从而避免函数被过于频繁地调用。
function throttle(func, ms) {
let isThrottled = false,
savedArgs,
savedThis;
function wrapper() {
if (isThrottled) {
// 所有调用都被保存在 savedArgs/savedThis 中。
savedArgs = arguments;
savedThis = this;
return;
}
isThrottled = true;
func.apply(this, arguments);
setTimeout(function() {
isThrottled = false;
if (savedArgs) {
wrapper.apply(savedThis, savedArgs);
savedArgs = savedThis = null;
}
}, ms);
}
return wrapper;
}