循环打印红黄绿
上面来看一道比拟典型的问题,通过这个问题来比照几种异步编程办法:红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次;如何让三个灯一直交替反复亮灯?
三个亮灯函数:
function red() {console.log('red');
}
function green() {console.log('green');
}
function yellow() {console.log('yellow');
}
这道题简单的中央在于 须要“交替反复”亮灯,而不是“亮完一次”就完结了。
(1)用 callback 实现
const task = (timer, light, callback) => {setTimeout(() => {if (light === 'red') {red()
}
else if (light === 'green') {green()
}
else if (light === 'yellow') {yellow()
}
callback()}, timer)
}
task(3000, 'red', () => {task(2000, 'green', () => {task(1000, 'yellow', Function.prototype)
})
})
这里存在一个 bug:代码只是实现了一次流程,执行后红黄绿灯别离只亮一次。该如何让它交替反复进行呢?
下面提到过递归,能够递归亮灯的一个周期:
const step = () => {task(3000, 'red', () => {task(2000, 'green', () => {task(1000, 'yellow', step)
})
})
}
step()
留神看黄灯亮的回调里又再次调用了 step 办法 以实现循环亮灯。
(2)用 promise 实现
const task = (timer, light) =>
new Promise((resolve, reject) => {setTimeout(() => {if (light === 'red') {red()
}
else if (light === 'green') {green()
}
else if (light === 'yellow') {yellow()
}
resolve()}, timer)
})
const step = () => {task(3000, 'red')
.then(() => task(2000, 'green'))
.then(() => task(2100, 'yellow'))
.then(step)
}
step()
这里将回调移除,在一次亮灯完结后,resolve 以后 promise,并仍然应用递归进行。
(3)用 async/await 实现
const taskRunner = async () => {await task(3000, 'red')
await task(2000, 'green')
await task(2100, 'yellow')
taskRunner()}
taskRunner()
手写 Promise
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
function MyPromise(fn) {
// 保留初始化状态
var self = this;
// 初始化状态
this.state = PENDING;
// 用于保留 resolve 或者 rejected 传入的值
this.value = null;
// 用于保留 resolve 的回调函数
this.resolvedCallbacks = [];
// 用于保留 reject 的回调函数
this.rejectedCallbacks = [];
// 状态转变为 resolved 办法
function resolve(value) {
// 判断传入元素是否为 Promise 值,如果是,则状态扭转必须期待前一个状态扭转后再进行扭转
if (value instanceof MyPromise) {return value.then(resolve, reject);
}
// 保障代码的执行程序为本轮事件循环的开端
setTimeout(() => {
// 只有状态为 pending 时能力转变,if (self.state === PENDING) {
// 批改状态
self.state = RESOLVED;
// 设置传入的值
self.value = value;
// 执行回调函数
self.resolvedCallbacks.forEach(callback => {callback(value);
});
}
}, 0);
}
// 状态转变为 rejected 办法
function reject(value) {
// 保障代码的执行程序为本轮事件循环的开端
setTimeout(() => {
// 只有状态为 pending 时能力转变
if (self.state === PENDING) {
// 批改状态
self.state = REJECTED;
// 设置传入的值
self.value = value;
// 执行回调函数
self.rejectedCallbacks.forEach(callback => {callback(value);
});
}
}, 0);
}
// 将两个办法传入函数执行
try {fn(resolve, reject);
} catch (e) {
// 遇到谬误时,捕捉谬误,执行 reject 函数
reject(e);
}
}
MyPromise.prototype.then = function(onResolved, onRejected) {
// 首先判断两个参数是否为函数类型,因为这两个参数是可选参数
onResolved =
typeof onResolved === "function"
? onResolved
: function(value) {return value;};
onRejected =
typeof onRejected === "function"
? onRejected
: function(error) {throw error;};
// 如果是期待状态,则将函数退出对应列表中
if (this.state === PENDING) {this.resolvedCallbacks.push(onResolved);
this.rejectedCallbacks.push(onRejected);
}
// 如果状态曾经凝固,则间接执行对应状态的函数
if (this.state === RESOLVED) {onResolved(this.value);
}
if (this.state === REJECTED) {onRejected(this.value);
}
};
实现双向数据绑定
let obj = {}
let input = document.getElementById('input')
let span = document.getElementById('span')
// 数据劫持
Object.defineProperty(obj, 'text', {
configurable: true,
enumerable: true,
get() {console.log('获取数据了')
},
set(newVal) {console.log('数据更新了')
input.value = newVal
span.innerHTML = newVal
}
})
// 输出监听
input.addEventListener('keyup', function(e) {obj.text = e.target.value})
Array.prototype.filter()
Array.prototype.filter = function(callback, thisArg) {if (this == undefined) {throw new TypeError('this is null or not undefined');
}
if (typeof callback !== 'function') {throw new TypeError(callback + 'is not a function');
}
const res = [];
// 让 O 成为回调函数的对象传递(强制转换对象)const O = Object(this);
// >>>0 保障 len 为 number,且为正整数
const len = O.length >>> 0;
for (let i = 0; i < len; i++) {
// 查看 i 是否在 O 的属性(会查看原型链)if (i in O) {
// 回调函数调用传参
if (callback.call(thisArg, O[i], i, O)) {res.push(O[i]);
}
}
}
return res;
}
实现类数组转化为数组
类数组转换为数组的办法有这样几种:
- 通过 call 调用数组的 slice 办法来实现转换
Array.prototype.slice.call(arrayLike);
- 通过 call 调用数组的 splice 办法来实现转换
Array.prototype.splice.call(arrayLike, 0);
- 通过 apply 调用数组的 concat 办法来实现转换
Array.prototype.concat.apply([], arrayLike);
- 通过 Array.from 办法来实现转换
Array.from(arrayLike);
手写 call 函数
call 函数的实现步骤:
- 判断调用对象是否为函数,即便咱们是定义在函数的原型上的,然而可能呈现应用 call 等形式调用的状况。
- 判断传入上下文对象是否存在,如果不存在,则设置为 window。
- 解决传入的参数,截取第一个参数后的所有参数。
- 将函数作为上下文对象的一个属性。
- 应用上下文对象来调用这个办法,并保留返回后果。
- 删除方才新增的属性。
- 返回后果。
// call 函数实现
Function.prototype.myCall = function(context) {
// 判断调用对象
if (typeof this !== "function") {console.error("type error");
}
// 获取参数
let args = [...arguments].slice(1),
result = null;
// 判断 context 是否传入,如果未传入则设置为 window
context = context || window;
// 将调用函数设为对象的办法
context.fn = this;
// 调用函数
result = context.fn(...args);
// 将属性删除
delete context.fn;
return result;
};
参考 前端进阶面试题具体解答
实现一个迷你版的 vue
入口
// js/vue.js
class Vue {constructor (options) {
// 1. 通过属性保留选项的数据
this.$options = options || {}
this.$data = options.data || {}
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
// 2. 把 data 中的成员转换成 getter 和 setter,注入到 vue 实例中
this._proxyData(this.$data)
// 3. 调用 observer 对象,监听数据的变动
new Observer(this.$data)
// 4. 调用 compiler 对象,解析指令和差值表达式
new Compiler(this)
}
_proxyData (data) {
// 遍历 data 中的所有属性
Object.keys(data).forEach(key => {
// 把 data 的属性注入到 vue 实例中
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get () {return data[key]
},
set (newValue) {if (newValue === data[key]) {return}
data[key] = newValue
}
})
})
}
}
实现 Dep
class Dep {constructor () {
// 存储所有的观察者
this.subs = []}
// 增加观察者
addSub (sub) {if (sub && sub.update) {this.subs.push(sub)
}
}
// 发送告诉
notify () {
this.subs.forEach(sub => {sub.update()
})
}
}
实现 watcher
class Watcher {constructor (vm, key, cb) {
this.vm = vm
// data 中的属性名称
this.key = key
// 回调函数负责更新视图
this.cb = cb
// 把 watcher 对象记录到 Dep 类的动态属性 target
Dep.target = this
// 触发 get 办法,在 get 办法中会调用 addSub
this.oldValue = vm[key]
Dep.target = null
}
// 当数据发生变化的时候更新视图
update () {let newValue = this.vm[this.key]
if (this.oldValue === newValue) {return}
this.cb(newValue)
}
}
实现 compiler
class Compiler {constructor (vm) {
this.el = vm.$el
this.vm = vm
this.compile(this.el)
}
// 编译模板,解决文本节点和元素节点
compile (el) {
let childNodes = el.childNodes
Array.from(childNodes).forEach(node => {
// 解决文本节点
if (this.isTextNode(node)) {this.compileText(node)
} else if (this.isElementNode(node)) {
// 解决元素节点
this.compileElement(node)
}
// 判断 node 节点,是否有子节点,如果有子节点,要递归调用 compile
if (node.childNodes && node.childNodes.length) {this.compile(node)
}
})
}
// 编译元素节点,解决指令
compileElement (node) {// console.log(node.attributes)
// 遍历所有的属性节点
Array.from(node.attributes).forEach(attr => {
// 判断是否是指令
let attrName = attr.name
if (this.isDirective(attrName)) {
// v-text --> text
attrName = attrName.substr(2)
let key = attr.value
this.update(node, key, attrName)
}
})
}
update (node, key, attrName) {let updateFn = this[attrName + 'Updater']
updateFn && updateFn.call(this, node, this.vm[key], key)
}
// 解决 v-text 指令
textUpdater (node, value, key) {
node.textContent = value
new Watcher(this.vm, key, (newValue) => {node.textContent = newValue})
}
// v-model
modelUpdater (node, value, key) {
node.value = value
new Watcher(this.vm, key, (newValue) => {node.value = newValue})
// 双向绑定
node.addEventListener('input', () => {this.vm[key] = node.value
})
}
// 编译文本节点,解决差值表达式
compileText (node) {// console.dir(node)
// {{msg}}
let reg = /\{\{(.+?)\}\}/
let value = node.textContent
if (reg.test(value)) {let key = RegExp.$1.trim()
node.textContent = value.replace(reg, this.vm[key])
// 创立 watcher 对象,当数据扭转更新视图
new Watcher(this.vm, key, (newValue) => {node.textContent = newValue})
}
}
// 判断元素属性是否是指令
isDirective (attrName) {return attrName.startsWith('v-')
}
// 判断节点是否是文本节点
isTextNode (node) {return node.nodeType === 3}
// 判断节点是否是元素节点
isElementNode (node) {return node.nodeType === 1}
}
实现 Observer
class Observer {constructor (data) {this.walk(data)
}
walk (data) {
// 1. 判断 data 是否是对象
if (!data || typeof data !== 'object') {return}
// 2. 遍历 data 对象的所有属性
Object.keys(data).forEach(key => {this.defineReactive(data, key, data[key])
})
}
defineReactive (obj, key, val) {
let that = this
// 负责收集依赖,并发送告诉
let dep = new Dep()
// 如果 val 是对象,把 val 外部的属性转换成响应式数据
this.walk(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get () {
// 收集依赖
Dep.target && dep.addSub(Dep.target)
return val
},
set (newValue) {if (newValue === val) {return}
val = newValue
that.walk(newValue)
// 发送告诉
dep.notify()}
})
}
}
应用
<!DOCTYPE html>
<html lang="cn">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Mini Vue</title>
</head>
<body>
<div id="app">
<h1> 差值表达式 </h1>
<h3>{{msg}}</h3>
<h3>{{count}}</h3>
<h1>v-text</h1>
<div v-text="msg"></div>
<h1>v-model</h1>
<input type="text" v-model="msg">
<input type="text" v-model="count">
</div>
<script src="./js/dep.js"></script>
<script src="./js/watcher.js"></script>
<script src="./js/compiler.js"></script>
<script src="./js/observer.js"></script>
<script src="./js/vue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data: {
msg: 'Hello Vue',
count: 100,
person: {name: 'zs'}
}
})
console.log(vm.msg)
// vm.msg = {test: 'Hello'}
vm.test = 'abc'
</script>
</body>
</html>
数组扁平化
数组扁平化是指将一个多维数组变为一个一维数组
const arr = [1, [2, [3, [4, 5]]], 6];
// => [1, 2, 3, 4, 5, 6]
办法一:应用 flat()
const res1 = arr.flat(Infinity);
办法二:利用正则
const res2 = JSON.stringify(arr).replace(/\[|\]/g, '').split(',');
但数据类型都会变为字符串
办法三:正则改进版本
const res3 = JSON.parse('[' + JSON.stringify(arr).replace(/\[|\]/g, '') +']');
办法四:应用 reduce
const flatten = arr => {return arr.reduce((pre, cur) => {return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
}, [])
}
const res4 = flatten(arr);
办法五:函数递归
const res5 = [];
const fn = arr => {for (let i = 0; i < arr.length; i++) {if (Array.isArray(arr[i])) {fn(arr[i]);
} else {res5.push(arr[i]);
}
}
}
fn(arr);
Promise 并行限度
就是实现有并行限度的 Promise 调度器问题
class Scheduler {constructor() {this.queue = [];
this.maxCount = 2;
this.runCounts = 0;
}
add(promiseCreator) {this.queue.push(promiseCreator);
}
taskStart() {for (let i = 0; i < this.maxCount; i++) {this.request();
}
}
request() {if (!this.queue || !this.queue.length || this.runCounts >= this.maxCount) {return;}
this.runCounts++;
this.queue.shift()().then(() => {
this.runCounts--;
this.request();});
}
}
const timeout = time => new Promise(resolve => {setTimeout(resolve, time);
})
const scheduler = new Scheduler();
const addTask = (time,order) => {scheduler.add(() => timeout(time).then(()=>console.log(order)))
}
addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');
scheduler.taskStart()
// 2
// 3
// 1
// 4
实现 jsonp
// 动静的加载 js 文件
function addScript(src) {const script = document.createElement('script');
script.src = src;
script.type = "text/javascript";
document.body.appendChild(script);
}
addScript("http://xxx.xxx.com/xxx.js?callback=handleRes");
// 设置一个全局的 callback 函数来接管回调后果
function handleRes(res) {console.log(res);
}
// 接口返回的数据格式
handleRes({a: 1, b: 2});
实现公布 - 订阅模式
class EventCenter{
// 1. 定义事件容器,用来装事件数组
let handlers = {}
// 2. 增加事件办法,参数:事件名 事件办法
addEventListener(type, handler) {
// 创立新数组容器
if (!this.handlers[type]) {this.handlers[type] = []}
// 存入事件
this.handlers[type].push(handler)
}
// 3. 触发事件,参数:事件名 事件参数
dispatchEvent(type, params) {
// 若没有注册该事件则抛出谬误
if (!this.handlers[type]) {return new Error('该事件未注册')
}
// 触发事件
this.handlers[type].forEach(handler => {handler(...params)
})
}
// 4. 事件移除,参数:事件名 要删除事件,若无第二个参数则删除该事件的订阅和公布
removeEventListener(type, handler) {if (!this.handlers[type]) {return new Error('事件有效')
}
if (!handler) {
// 移除事件
delete this.handlers[type]
} else {const index = this.handlers[type].findIndex(el => el === handler)
if (index === -1) {return new Error('无该绑定事件')
}
// 移除事件
this.handlers[type].splice(index, 1)
if (this.handlers[type].length === 0) {delete this.handlers[type]
}
}
}
}
JSONP
script 标签不遵循同源协定,能够用来进行 跨域申请,长处就是兼容性好但仅限于 GET 申请
const jsonp = ({url, params, callbackName}) => {const generateUrl = () => {
let dataSrc = '';
for (let key in params) {if (Object.prototype.hasOwnProperty.call(params, key)) {dataSrc += `${key}=${params[key]}&`;
}
}
dataSrc += `callback=${callbackName}`;
return `${url}?${dataSrc}`;
}
return new Promise((resolve, reject) => {const scriptEle = document.createElement('script');
scriptEle.src = generateUrl();
document.body.appendChild(scriptEle);
window[callbackName] = data => {resolve(data);
document.removeChild(scriptEle);
}
})
}
实现千位分隔符
// 保留三位小数
parseToMoney(1234.56); // return '1,234.56'
parseToMoney(123456789); // return '123,456,789'
parseToMoney(1087654.321); // return '1,087,654.321'
function parseToMoney(num) {num = parseFloat(num.toFixed(3));
let [integer, decimal] = String.prototype.split.call(num, '.');
integer = integer.replace(/\d(?=(\d{3})+$)/g, '$&,');
return integer + '.' + (decimal ? decimal : '');
}
手写类型判断函数
function getType(value) {
// 判断数据是 null 的状况
if (value === null) {return value + "";}
// 判断数据是援用类型的状况
if (typeof value === "object") {let valueClass = Object.prototype.toString.call(value),
type = valueClass.split("")[1].split("");
type.pop();
return type.join("").toLowerCase();} else {
// 判断数据是根本数据类型的状况和函数的状况
return typeof value;
}
}
用正则写一个依据 name 获取 cookie 中的值的办法
function getCookie(name) {var match = document.cookie.match(new RegExp('(^|)' + name + '=([^;]*)'));
if (match) return unescape(match[2]);
}
- 获取页面上的
cookie
能够应用document.cookie
这里获取到的是相似于这样的字符串:
'username=poetry; user-id=12345; user-roles=home, me, setting'
能够看到这么几个信息:
- 每一个 cookie 都是由
name=value
这样的模式存储的 - 每一项的结尾可能是一个空串
''
(比方username
的结尾其实就是), 也可能是一个空字符串' '
(比方user-id
的结尾就是) - 每一项用
";"
来辨别 - 如果某项中有多个值的时候,是用
","
来连贯的 (比方user-roles
的值) - 每一项的结尾可能是有
";"
的(比方username
的结尾),也可能是没有的 (比方user-roles
的结尾) - 所以咱们将这里的正则拆分一下:
'(^|)'
示意的就是获取每一项的结尾,因为咱们晓得如果^
不是放在[]
里的话就是示意结尾匹配。所以这里(^|)
的意思其实就被拆分为(^)
示意的匹配username
这种状况,它后面什么都没有是一个空串 (你能够把(^)
了解为^
它前面还有一个暗藏的''
);而|
示意的就是或者是一个" "
(为了匹配user-id
结尾的这种状况)+name+
这没什么好说的=([^;]*)
这里匹配的就是=
前面的值了,比方poetry
;刚刚说了^
要是放在[]
里的话就示意"除了 ^ 前面的内容都能匹配"
,也就是非的意思。所以这里([^;]*)
示意的是除了";"
这个字符串别的都匹配 (*
应该都晓得什么意思吧,匹配 0 次或屡次)- 有的大佬等号前面是这样写的
'=([^;]*)(;|$)'
,而最初为什么能够把'(;|$)'
给省略呢?因为其实最初一个cookie
项是没有';'
的,所以它能够合并到=([^;]*)
这一步。 - 最初获取到的
match
其实是一个长度为 4 的数组。比方:
[
"username=poetry;",
"","poetry",";"
]
- 第 0 项:全量
- 第 1 项:结尾
- 第 2 项:两头的值
- 第 3 项:结尾
所以咱们是要拿第 2 项 match[2]
的值。
- 为了避免获取到的值是
%xxx
这样的字符序列,须要用unescape()
办法解码。
手写 Promise.then
then
办法返回一个新的 promise
实例,为了在 promise
状态发生变化时(resolve
/ reject
被调用时)再执行 then
里的函数,咱们应用一个 callbacks
数组先把传给 then 的函数暂存起来,等状态扭转时再调用。
那么,怎么保障后一个 **then**
里的办法在前一个 **then**
(可能是异步)完结之后再执行呢? 咱们能够将传给 then
的函数和新 promise
的 resolve
一起 push
到前一个 promise
的 callbacks
数组中,达到承前启后的成果:
- 承前:以后一个
promise
实现后,调用其resolve
变更状态,在这个resolve
里会顺次调用callbacks
里的回调,这样就执行了then
里的办法了 - 启后:上一步中,当
then
里的办法执行实现后,返回一个后果,如果这个后果是个简略的值,就间接调用新promise
的resolve
,让其状态变更,这又会顺次调用新promise
的callbacks
数组里的办法,周而复始。。如果返回的后果是个promise
,则须要等它实现之后再触发新promise
的resolve
,所以能够在其后果的then
里调用新promise
的resolve
then(onFulfilled, onReject){
// 保留前一个 promise 的 this
const self = this;
return new MyPromise((resolve, reject) => {
// 封装前一个 promise 胜利时执行的函数
let fulfilled = () => {
try{const result = onFulfilled(self.value); // 承前
return result instanceof MyPromise? result.then(resolve, reject) : resolve(result); // 启后
}catch(err){reject(err)
}
}
// 封装前一个 promise 失败时执行的函数
let rejected = () => {
try{const result = onReject(self.reason);
return result instanceof MyPromise? result.then(resolve, reject) : reject(result);
}catch(err){reject(err)
}
}
switch(self.status){
case PENDING:
self.onFulfilledCallbacks.push(fulfilled);
self.onRejectedCallbacks.push(rejected);
break;
case FULFILLED:
fulfilled();
break;
case REJECT:
rejected();
break;
}
})
}
留神:
- 间断多个
then
里的回调办法是同步注册的,但注册到了不同的callbacks
数组中,因为每次then
都返回新的promise
实例(参考下面的例子和图) - 注册实现后开始执行构造函数中的异步事件,异步实现之后顺次调用
callbacks
数组中提前注册的回调
字符串最长的不反复子串
题目形容
给定一个字符串 s,请你找出其中不含有反复字符的 最长子串 的长度。示例 1:
输出: s = "abcabcbb"
输入: 3
解释: 因为无反复字符的最长子串是 "abc",所以其长度为 3。示例 2:
输出: s = "bbbbb"
输入: 1
解释: 因为无反复字符的最长子串是 "b",所以其长度为 1。示例 3:
输出: s = "pwwkew"
输入: 3
解释: 因为无反复字符的最长子串是 "wke",所以其长度为 3。请留神,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。示例 4:
输出: s = ""
输入: 0
答案
const lengthOfLongestSubstring = function (s) {if (s.length === 0) {return 0;}
let left = 0;
let right = 1;
let max = 0;
while (right <= s.length) {let lr = s.slice(left, right);
const index = lr.indexOf(s[right]);
if (index > -1) {left = index + left + 1;} else {lr = s.slice(left, right + 1);
max = Math.max(max, lr.length);
}
right++;
}
return max;
};
Promise.all
Promise.all
是反对链式调用的,实质上就是返回了一个 Promise 实例,通过 resolve
和reject
来扭转实例状态。
Promise.myAll = function(promiseArr) {return new Promise((resolve, reject) => {const ans = [];
let index = 0;
for (let i = 0; i < promiseArr.length; i++) {promiseArr[i]
.then(res => {ans[i] = res;
index++;
if (index === promiseArr.length) {resolve(ans);
}
})
.catch(err => reject(err));
}
})
}
Object.is
Object.is
解决的次要是这两个问题:
+0 === -0 // true
NaN === NaN // false
const is= (x, y) => {if (x === y) {
// + 0 和 - 0 应该不相等
return x !== 0 || y !== 0 || 1/x === 1/y;
} else {return x !== x && y !== y;}
}
手写 apply 函数
apply 函数的实现步骤:
- 判断调用对象是否为函数,即便咱们是定义在函数的原型上的,然而可能呈现应用 call 等形式调用的状况。
- 判断传入上下文对象是否存在,如果不存在,则设置为 window。
- 将函数作为上下文对象的一个属性。
- 判断参数值是否传入
- 应用上下文对象来调用这个办法,并保留返回后果。
- 删除方才新增的属性
- 返回后果
// apply 函数实现
Function.prototype.myApply = function(context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {throw new TypeError("Error");
}
let result = null;
// 判断 context 是否存在,如果未传入则为 window
context = context || window;
// 将函数设为对象的办法
context.fn = this;
// 调用办法
if (arguments[1]) {result = context.fn(...arguments[1]);
} else {result = context.fn();
}
// 将属性删除
delete context.fn;
return result;
};