1.实现一个函数printNum,接受一个参数n,输出n个递增自然数
输出的自然数不能含有7(17,71,176)或为7的倍数(14,63);如果含有7或为7的倍数,则输出下一个自然数;
· 支持多次调用,从0开始,每次自上次调用的末尾自然数继续打印
· 例子:
printNum(); // 0
printNum(2); // 1,2
printNum(5); // 3,4,5,6,8
printNum(17); // 9,10,11,...,26,29,30
思路:
1.对参数为undefined特殊处理
2.输入的参数为输出数字的长度
3.对数字进行判断不能含有7或7的倍数
4.需要对每次最后输出的值进行记忆
5.将数组转为字符串输出
考察知识点:
1.数组方法的使用
2.正则基本功
代码:
let lastNum = 0
function printNum (num) {
if (!num) {
return 0
}
let arr = []
for (let i = 0; i < num; i++ ) {
lastNum ++
if (lastNum % 7 === 0 || /[7]/.test(lastNum) ) {
num ++
// 规避了 27 28这种情况
}
if (lastNum % 7 !== 0 && /[7]/.test(lastNum) ) {
arr.push(lastNum)
}
}
return arr.toString()
}
最终输出结果与题目一致,但此解法并不完美,1.判断了两次7的倍数和含有7的情况 2.动态改变了for循环的num值
优化
let i = -1
function printNum (n) {
let res = []
res.push(++i)
while(n > 1) {
i++
if (i % 7 != 0 && !(/[7]/.test(i))) {
res.push(i)
n--
}
}
return res.join(',')
}
用闭包实现
function printNum (n) {
let i = -1
return function temp (n) {
let res = []
++i
if (i % 7 === 0 || /[7]/.test(i)) {
printNum(n)
}
res.push(i)
if (n < 0) {
return res.push(i)
}
while (n > 1) {
i++
if (i % 7 !== 0 && !(/[7]/.test(i)) {
res.push(i)
n--
}
}
return res.join(',')
}
}
2.实现一个函数,接受一个url数组作为参数,返回一个Promise。
- 要求同时发送请求获取资源内容(发送请求使用Fetch,fetch函数说明:fetch()必须接受一个参数-----资源的路径。无论请求成功与否,它都返回一个promise对象,resolve对应请求的Response)
- 当任意一个资源加载成功(通过请求的status状态即可)则将promise的状态置为resolve,并将该url作为resolve的参数
- 当所有的资源都不成功时,promise状态置为reject
题目解析(感觉这道题目不是重点)
1.参数为url数组
2.同时发送请求(让我想到了预加载图片、上传多个文件)
- 写出如下输出值
this.a = 20
var test = {
a: 40,
init: () => {
console.log(this.a)
function go () {
console.log(this.a)
}
go.prototype.a = 50
return go
}
}
var p = test.init() // 20 先执行 . 再执行 ()
p() // 20
箭头里的this指向顶级对象test的作用域,指向window
this.a = 20
var test = {
a: 40,
init: function () {
console.log(this.a)
function go () {
console.log(this.a)
}
go.prototype.a = 50
return go
}
}
test.init() // 40
var handler = {
id: '123456',
init: function() {
document.addEventListener('click',
event => console.log(this), false);
}
}
handler.init() // 调用打印的this是hander对象
var handler = {
id: '123456',
init: ()=> {
console.log(this)
}
}
handler.init() // 调用打印的this是window
3.分别使用递归和迭代的方式实现斐波那契数列值的计算: F(0) = 0,F(1) = 1,F(n) = F(n - 1) + F(n - 2);(n>=2, n为正整数)
// 递归
function fib (n) {
if (n == 1 || n == 2) {
return 1
}
return fib(n-1) + fib(n-2)
}
fib(10) // 55
时间复杂度为O(2^n),空间复杂度为O(n)
// 非递归
function fib (n) {
var res = [1, 1]
if (n === 1 || n ===2) {
return 1
}
for (var i = 2; i < n; i++) {
res[i] = res[i-1] + res[i-2]
}
return res[n-1]
}
fib(10) // 55
时间复杂度为O(n),空间复杂度为O(n)
for循环版本(递归有性能问题,用循环来做。)
function fibonacci (n) {
var last = 1
var last2 = 0
var current = last2
for (var i = 1; i <= n; i++) {
last2 = last
last = current
current = last + last2
}
return current
}
// 非递归
function fib (n) {
var a, b ,res;
a = b = 1;
for (var i = 3;i <= n; i++) {
res = a + b
a = b
b = res
}
return res
}
时间复杂度为O(n),空间复杂度为O(1)
function fibon(a,b,n) {
if (n<=2){
return a;
}
return fibon(a+b,a,n-1);
}
console.log(fibon(1,1,10));//求数列中第十个数
4.bind 看官方的polyfill
- bind
- arguments
- 原型
if (!Function.prototype.bind) {
Function.prototype.bind = function (onThis) {
if(typeof this != "function") {
throw new Error("请使用函数调用bind")
}
var args = Arrays.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () {},
fBound = function () {
console.log('args', args)
const _args = args.concat(Array.prototype.slice.call(arguments))
// bind 核心, this instanctof fBound == true的时候,说明返回的fBound被当做new的构造函数调用
// 模拟原生bind的多个参数
return fToBind.apply( this instanctof fBound ? this : onThis, _args)
}
// 维护原型链
if (this.prototype) {
fNOP.prototype = this.prototype
}
// 同一个堆地址 fBound.prototype = this.prototype 是错误的
// 修正函数名和构造函数
fBound.prototype = new fNOP()
return fBound
}
}
this.age = 18
function people (data,data1) {
this.cons = "构造函数"
console.log(this.age)
console.log(data)
console.log(data1)
}
test.prototype.init = function () {
console.log()
}
var result = test.bind(this, "Fruit")
var result1 = new result("Fruit Bro")
5.异步错误能被捕获到吗
第一种
try {
console.log(1)
setTimeout(() => {
console.log(2)
}, 100);
setTimeout(() => {
console.log(3)
throw new Error(5)
}, 0);
console.log(4)
} catch (error) {
console.log(`错啦 ${error}`)
}
第二种
try {
console.log(1)
setTimeout(() => {
console.log(2)
}, 100);
console.log(3)
throw new Error(5)
console.log(4)
} catch (error) {
console.log(`错啦 ${error}`)
}
自己先猜想一下吧,然后再控制台验证一下自己的猜想
执行结果如下
- 第一种
分析:运行中直接抛出了错误,而没有进入catch,说明异常中的错误未被正常捕获到,那么,问题来了,对于异步的错误改如何处理呢?欢迎说出你的想法
- 第二种
可以发现,正常打印了 【错啦 Error: 5】说明错误被catch捕获到了,对于同的错误的捕获没有任何问题
6.实现函数柯里化
function composeFunctions () { var args = Array.prototype.slice.apply(arguments)
var _func = fucntion () {
if (arguments.length === 0) {
return func.apply(this, args)
}
Array.prototype.push.apply(args, arguments)
return _func
}
return _func
}
7.简短优雅地实现sleep函数
async function test() {
console.log('hello')
await sleep(1000)
console.log('world')
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
test()
8.防抖
防抖就是最终只调用一次。
防抖和节流都是用闭包的方式来实现的。
防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于wait,防抖的情况下只会调用一次,而节流的情况会每隔一定时间(参数wait)调用函数。
// func是用户传入需要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => {
// 缓存一个定时器id
let timer = 0
// 这里返回的函数是每次用户实际调用的防抖函数
// 如果已经设定过定时器了就清空上一次的定时器
// 开始一个新的定时器,延迟执行用户传入的方法
return function (...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
const handleClick = () => {
// 触发相关操作
}
// 使用
var m = debounce(handleClick, 100) // 返回节流函数
// 在100毫秒的时间内调用m,就会被清除
使用
function handleClick (...args) {
//...
}
let a = debounce(handleClick, 1000)
// 每次执行时调用a就可以了
来实现一个带有立即执行选项的防抖函数
例如用户点star的时候,我们希望用户点第一下的时候就去调用接口,并且成功之后改变star按钮的样子,用户就可以立马得到反馈是否star成功了,这个情况适用立即执行的防抖函数,它总是在第一次调用,并且下一次调用必须与前一次调用的时间间隔大于wait才会触发。
// 获取当前时间戳
function now () {
return +new Date()
}
/**
* 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
*
* @param {function} func 回调函数
* @param {number} wait 表示时间窗口的间隔
* @param {boolean} immediate 设置为ture时,是否立即调用函数
* @return {function} 返回客户调用函数
*/
function debounce (func, wait = 50, immediate = true) {
let timer, context, args
// 延迟执行函数
const later = () => setTimeout(() => {
// 延迟函数执行完毕,清空缓存的定时器序号
timer = null
// 延迟执行的情况下,函数会在延迟函数中执行
// 使用到之前缓存的参数和上下文
if (!immediate) {
func.apply(context, args)
context = args = null
}
}, wait)
// 这里返回的函数是每次实际调用的函数
return function(...params) {
// 如果没有创建延迟执行函数(later),就创建一个
if (!timer) {
timer = later()
// 如果是立即执行,调用函数
// 否则缓存参数和调用上下文
if (immediate) {
func.apply(this, params)
} else {
context = this
args = params
}
// 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
// 这样做延迟函数会重新计时
} else {
clearTimeout(timer)
timer = later()
}
}
}
总结:
- 对于按钮防点击来说的实现:如果函数是立即执行的,就立即调用,如果函数式延迟执行的,就缓存上下文和参数,放到延迟函数中去执行。一旦我开始一个定时器,只要我定时器还在,你每次点击我都重新计时。一旦你点累了,定时器时间到,定时器重置为null,就可以再次点击了。
- 对于延时执行函数来说的实现:清除定时器ID,如果是延迟调用就调用函数。
9.节流
节流是每隔一段时间执行一次。
考虑一个场景,滚动事件中会发起网络请求,但是我们并不希望用户在滚动过程中一直发起请求,而是隔一段时间发起一次,对于这种情况我们就可以使用节流。
防抖和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。
// func是用户传入需要防抖的函数
// wait是等待时间
const throttle = (func, wait = 50) => {
// 上一次执行该函数的时间
let lastTime = 0
return function(...args) {
// 当前时间
let now = +new Date() // 转化为毫秒
// 将当前时间和上一次执行函数时间对比
// 如果差值大于设置的等待时间就执行函数
if (now - lastTime > wait) {
lastTime = now
// 改变this
func.apply(this, args)
}
}
}
// 假如这里是触发的函数,不停在触发
setInterval(
throttle(() => {
console.log(1)
}, 500),
1
)
/**
* underscore 节流函数,返回函数连续调用时,func 执行频率限定为 次 / wait
*
* @param {function} func 回调函数
* @param {number} wait 表示时间窗口的间隔
* @param {object} options 如果想忽略开始函数的的调用,传入{leading: false}。
* 如果想忽略结尾函数的调用,传入{trailing: false}
* 两者不能共存,否则函数不能执行
* @return {function} 返回客户调用函数
*/
_.throttle = function(func, wait, options) {
var context, args, result
var timeout = null
// 之前的时间戳
var previous = 0
// 如果 options 没传则设为空对象
if (!option) options = {}
// 定时器回调函数
var later = function () {
// 如果设置了 leading,就将 previos 设为 0
// 用于下面函数的第一个 if 判断
previous = options.leading === false ? 0 : _.now()
// 置空一是为了防止内存泄漏,二是位了下面的定时器判断
timeout = null
result = func.apply(context, args)
if (!timeout) context = args = null
}
return function () {
// 获得当前时间戳
var now = _.now()
// 首次进入前者肯定为 true
// 如果需要第一次不执行函数
// 就将上次时间戳设为当前的
// 这样在接下来计算 remaining 的值时会大于0
if (!previous && options.leading === false) previous = now
// 计算剩余时间
var remaining = wait - (now - previous)
context = this
args = arguments
// 如果当前调用已经大于上次调用时间 + wait
// 或者用户手动调了时间
// 如果设置了 trailing,只会进入这个条件
// 如果没有设置 leading,那么第一次会进入这个条件
// 还有一点,你可能会觉得开启了定时器那么应该不会进入这个 if 条件了
// 其实还是会进入的,因为定时器的延时
// 并不是准确的时间,很可能你设置了2秒
// 但是他需要2.2秒才触发,这时候就会进入这个条件
if (remaining <= 0 || remaining > wait) {
// 如果存在定时器就清理掉否则会调用二次回调
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
result = func.apply(context, args)
if (!timeout) context = args = null
} esle if (!timeout && options.trailing !== false) {
// 判断是否设置了定时器和 trailing
// 没有的话就开启一个定时器
// 并且不能不能同时设置 leading 和 trailing
timeout = setTimeout(later, remaining)
}
return result
}
}
10.变量提升
test()
alert(a) // undefined
var flag = true
if (!flag) {
var a = 1
}
if (flag) {
function test () {
console.log('test1')
}
} else {
function test () {
console.log('test2')
}
}
function test () {
console.log('1')
}
function init () {
test()
if (false) {
function test () {
console.log('2')
}
}
}
init()
function func(flag) {
if (flag) {
function getValue() {
return 'a'
}
} else {
function getValue () {
return 'b'
}
}
return getValue()
}
console.log(func(true))
// 上面这段代码相当于
function functions(flag) {
function getValue(){return 'a'};
function getValue(){return 'b'};
if (flag) {
....
} else {
....
}
return getValue();
}
// 如何解决
function functions(flag) {
if (flag) {
var getValue = function () { return 'a'; }
} else {
var getValue = function () { return 'b'; }
}
return getValue();
}
// 用函数表达式的方法,因为函数表达式不会提升,因此只有当逻辑执行到这儿的时候才会执行。避免了函数声明提升的问题。
console.log(a) // f a() {}
var a = 1
function a () {
}
console.log(a) // 1
11. 在一个数组中找出和为m的两个数,拓展一下,一个数组中找出n个数,使其和为m
假设:找出和为10的两个数
const arr = [5, 6, 2, 1, 7, 9, 4, 8]
解法一: 穷举
1.将数组排序
2.while循环
const arr = [5, 6, 2, 1, 7, 9, 4, 8]
function sortArr (arr) {
return arr.sort((a, b) => {
return a - b
})
}
function getSumNum (arr, Sum) {
let newArr = sortArr(arr)
let i = 0
let j = arr.length -1
while (i < j) {
if (i === j) return
if (newArr[i] + newArr[j] === Sum) {
console.log( `${newArr[i]} 和 ${newArr[j]}` )
i++
j--
} else if (newArr[i] + newArr[j] < Sum) {
i++
} else {
j--
}
}
return 'none'
}
// 1 和 9
// 2 和 8
// 4 和 6
// none
12. 反转字符串
输入: "Let's take LeetCode contest"
输出: "s'teL ekat edoCteeL tsetnoc"
// 解法1. 基础解法
var reverseWords = function(s) {
// 字符串按空格进行分割
return s.split(' ').map(item => {
return item.split('').reverse().join('')
}).join(' ')
}
// 解法2. 巧妙解法,执行速度比map快
var reverseWords = function(s) {
let arr = s.split('').reverse().join('') // 注意split和join的单引号没有空格
return arr.split(' ').reverse().join(' ') //
注意split和join的单引号有空格
}
// 解法3. 正则
// \s+代表空格 或 /\s/g, \s+中+代表大于一次
s.split('').reverse().join('').split(/\s+/).reverse().join(' ')
s.split('').reverse().join('').split(/\s/g).reverse().join(' ')
var reverseWords = function(s) {
// 字符串按空格进行分割
return s.match(/[\w']+/g).map(item => {
return item.split('').reverse().join('')
}).join(' ')
}
// 解法4. match
var reverseWords = function(s) {
// 字符串按空格进行分割
return s.match(/[\w']+/g).map(item => {
return item.split('').reverse().join('')
}).join(' ')
}
涉及到的API developer.mozilla.org/zh-CN/docs/…
developer.mozilla.org/zh-CN/docs/…
String.prototype.match 方法检索返回一个字符串匹配正则表达式的的结果
match中 [\w'] w代表单词 '表示包含'单引号
String.prototype.split
String.prototype.match
Array.prototype.map
Array.prototype.reverse
Array.prototype.join
split:使分离,离开,分解
熟悉所有API,知道在某些场景的实际应用
13. 计数二进制子串
leetcode 给定一个字符串 s,计算具有相同数量0和1的非空(连续)子字符串的数量,并且这些子字符串中的所有0和所有1都是组合在一起的。
重复出现的子串要计算它们出现的次数。
科学就是发现自然界的规律,并提炼出公式
算法的本质是寻找规律并实现
如何找到规律?发现输入和输出的关系,寻找突破点
复杂的实现怎么办?实现是程序+数据结构的结合体
规律,从前往后依次找,找到一个结果向下一位。子输入,输入是动态的,每一次输入都少一位。因此需要用到递归。找子串的行为可以抽出来,是一个公共的行为。
00110011
00110011
00110011
00110011
00110011
00110011
程序的本质就是for循环、if else、switch case、递归...
输入: "00110011"
输出: 6
解释: 有6个子串具有相同数量的连续1和0:“0011”,“01”,“1100”,“10”,“0011” 和 “01”。
输入: "10101"
输出: 4
解释: 有4个子串:“10”,“01”,“10”,“01”,它们具有相同数量的连续1和0。
var countBinarySubstrings = function(s) {
};
for(i=0; i< str.length-1;i++) {
r = match(str.slice(i))
if(r) {
result.push(r)
}
}
找规律这种事只能靠自己去悟
正则很重要
// 使用正则
var countBinarySubstrings = function (s) {
const result = [] // 队列,建立数据结构,堆栈,保存数据
// 给定任意子输入都返回第一个符合条件的子串
let match = (str) => {
// 如何找第一个子串,是非常难的点。。。
// 1. 先找到了连续的数0或1
let j = str.match(/^(0+|1+)/)[0]
// 找到两个连续的0,就要有两个连续的1
let o = (j[0] ^ 1).toString().repeat(j.length) // 按位操作符,取反,只用取出第一位就可以。再进行补齐
let reg = new RegExp(`^(${j}${o})`)
if (reg.test(str)) {
return RegExp.$1
} else {
return ''
}
}
// 递归 for循环
// 控制运行流程
for (let i = 0; i < s.length - 1; i++) {
// 从第一位开始,找到符合条件(连续0和1)的数据
// slice:从i开始,截取字符串到最后
let sub = match(s.slice(i))
if (sub) {
result.push(sub)
}
}
return result.length
}
// 不使用正则
var countBinarySubstrings = function (s) {
const result = []
let match = (str) => {
let j = str[0]
let m
for (let i = 1; i < str.length; i++) {
if (str[i] === j) {
if (!m) {
m = str[i]
} else {
m = m + str[i]
}
} else {
break
}
}
if (m) {
j = j + m
}
let o = (j[0] === '0' ? '1' : '0').repeat(j.length)
let substr = str.includes(j + o)
if (substr) {
return j + o
} else {
return ''
}
}
for (let i = 0; i < s.length - 1; i++) {
let sub = match(s.slice(i))
if (sub) {
result.push(sub)
}
}
return result.length
}
涉及到的API
String.prototype.slice
String.prototype.match
String.prototype.repeat
Array.ptototype.push
RegExp
参考文档:正则表达式
14. 分别使用递归和迭代的方式实现菲波那切数列值的计算: F(0)= 0, F(1) = 1, F(n) = F(n-1) + F(n-2),(n >= 2)
通向公式就是F(n) = F(n-1) + F(n-2)
// 使用递归
function fib(n) {
if (n === 0) {
return 0
}
if (n === 1) {
return 1
}
return fib(n - 1) + fib(n - 2)
}
// for循环: 将n变为0和1
function fib(n) {
if (n === 0) {
return 0
}
if (n === 1) {
return 1
}
// 声明3个变量
var last = 1 // f(1) 上一次的值
var last2 = 0 // f(0) 上上一次的值
var current = last2 // 保存当前求和的值,初始为0
for(var i = 1; i <= n; i++) {
// 上上一次的值 就是 上一次的值
last2 = last
// 上一次的值 就是 当前的和
last = current
// 最终的值就是上一次和上上一次值的和
current = last + last2
}
return current
}
15. 字符串数字求和
"1234" + "567" = "1801"
核心难点在于“逢十进一”,如下的解题思路是将数据进行了反转,需要“逢十进一”的时候,提前对下一位赋值“1”,然后在下一次循环的时候对其进行+1操作,比如sum为11,对其+1之后就是12,则2就是下一位的值
参考文章: www.jianshu.com/p/071a31606…
思路:
- 将字符串转为数组
- 将对应位置数据求和并判断是否需要逢十进一
function add(a, b) {
var number1
var number2
var MAX_LENGTH
var result = []
if(a.length > b.length) {
number1 = a.split('').reverse()
number2 = b.split('').reverse()
MAX_LENGTH = a.length
} else {
number1 = b.split('').reverse()
number2 = a.split('').reverse()
MAX_LENGTH = b.length
}
for (let i = 0; i < MAX_LENGTH; i++) {
var sum
// 判断数字较短的是否还有值
if (number2[i]) {
// 两个数字在当前位置都有值
sum = Number(number1[i]) + Number(number2[i])
} else {
// 较小的值在当前位置没有值
sum = Number(number1[i])
}
// 先判断result数组当前位置是否还有值,此时还未对result[i]进行赋值,如果有值,则说明在上次循环中,已经逢十进一
if (result[i]) {
// 上个位数计算中逢十进一的判断
sum += 1 // 当前位置需要逢十进一,则需要加1
}
result[i] = String(sum % 10) // 拿到i位置的值
if(sum >= 10) {
// 说明下一位需要逢十进一,提前对下一位赋值1,既是一个flag,又是用在上面的if判断中,因为对于“逢十进一”,下一位最大就是1,这个地方真的是很巧妙,也解决了'12'+ '91' = '103'这种只能循环两次,但需要位数要从二位进到三位的情况
result[i + 1] = '1'
}
}
return result.reverse().join('')
}
以下写法不能满足'12'+ '91' = '103'这种需要进位的情况
function sum(a, b) {
var aArr = a.split('')
var bArr = b.split('')
var maxArr
var shortArr
var resultStr
if(aArr.length > bArr.length) {
maxArr = aArr
shortArr = bArr
} else {
maxArr = bArr
shortArr = aArr
}
var maxLen = maxArr.length
var shortLen = shortArr.length
var flag = false
for(var i = 0; i < maxLen; i++ ) {
var number = Number(aArr[maxLen - i - 1]) + (Number(bArr[shortLen - i - 1]) || 0)
if(flag) {
number += 1
}
var currentStr = String(number)
// var str
if (number > 9) {
flag = true
currentStr = String(number).substring(1)
} else {
flag = false
}
// 遗漏了提前对下一位赋值1的问题
resultStr = resultStr ? currentStr + resultStr : currentStr
}
return resultStr
}
16. 动手实现一个repeat方法
例如
function repeat(func, times, wait) {
// TODO
}
const repeatFunc = repeat(alert, 4, 3000)
// 调用这个 repeatFunc("helloworld"),会alert 4次 helloworld,每次间隔3秒
- 使用
setInterval,执行达到次数时,清除setInterval
function repeat(func, times, wait) {
if (typeof func !== 'function') return
if (times <= 0) return
return value => {
let timesTmp = times
let interval = setInterval(() => {
func(value)
tiemsTmp--
timesTmp === 0 && clearInterval(interval)
}, wait)
}
}
const repeatFunc = repeat(alert, 4, 3000)
repeatFunc('helloworld')
function repeat(func, times, wait) {
return function(...args) {
let i = 0
let _args = [...args]
let handle = setInterval(() => {
i += 1
if (i > times) {
clearInterval(handle)
return
}
func.apply(null, _args)
}, wait)
}
}
const repeatFunc = repeat(alert, 4, 3000)
repeatFucn('helloworld')
17. 一个人每次只能走一层楼梯或者两层楼梯,问走到第88层楼梯一共有多少种方法
持续更新...