JavaScript练习任务2

对象的值翻倍(考点对象与数组的转换)

有一个带有价格的 prices 对象。

  1. 编写函数 doublePrices (prices),使单价翻倍
  2. 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个对象roommeetup,他们互相引用对方。编写 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 的和。
用三种方式实现:

  1. 使用循环。
  2. 使用递归,对 n > 1 执行 sumTo(n) = n + sumTo(n-1)
  3. 使用 等差数列 求和公式: 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 结束。

  1. 使用 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);
  1. 使用嵌套的 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"

为什么定时器使用箭头函数?
箭头函数没有自己的 thisarguments,所以 func.apply(this, arguments) 从包装器中获取 thisarguments

防抖装饰器(函数防抖)

创建一个防抖装饰器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;
}
  • 22
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值