- 实际常用
- 节流函数throttle
- 防抖函数debounce
- 深拷贝
- 简单的jquery, 考虑插件和扩展性
- 深拷贝( 破解递归爆栈 )
- 深拷贝( 破解循环引用 )
- 使用setTimeout模拟setInterval
- js实现一个继承方法
- 手写ajax
- http请求封装
- 注意一下 vue 中 mixins 的优先级,component > mixins > extends。
- 介绍下深度优先遍历和广度优先遍历,如何实现?
- 实现sleep函数(使用Promise封装setTimeout)
- 使用 setTimeout 实现 setInterval
- 判断对象是否存在循环引用
- 手写一个观察者模式
- 手写一个观察者模式
- 手写去重
- 使用闭包实现每隔一秒打印 1,2,3,4
- 手写一个 jsonp
- 实现 compose 函数
- 请用js去除字符串空格
- 反转数组
- 要求
- 要求
- 要求
- 要求
实际常用
节流函数throttle
预定一个函数只有在大于等于执行周期时才执行,周期内调用不执行。就好像你在淘宝抢购某一件限量热卖商品时,你不断点刷新点购买,可是总有一段时间你点上是没有效果,这里就用到了节流,就是怕点的太快导致系统出现bug。
function throttle(fn, delay = 500) {
let isCall = true;
return function (...args) {
if (!isCall) return;
isCall = false; // 不能进来了
setTimeout(() => {
fn.apply(this, args);
isCall = true;
}, delay); // 第一次不调用
};
}
// 这种第一次会调用
function throttle2(fn, delay = 500) {
let oldTime = 0;
return function (...args) {
if (oldTime + delay <= Date.now()) {
oldTime = Date.now();
fn.apply(this, args);
}
};
}
防抖函数debounce
在函数需要频繁触发时,只有当有足够空闲的时间时,才执行一次。就好像在百度搜索时,每次输入之后都有联想词弹出,这个控制联想词的方法就不可能是输入框内容一改变就触发的,他一定是当你结束输入一段时间之后才会触发。
function debounce(fn, delay = 500) {
let timer;
return function (...args) {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
深拷贝
function deepClone(obj,type=1){
// 如果不是Object类型或者没有值 直接原样返回
if (typeof obj !== "object" || obj==null) {
return obj
}
if(type==1){
return JSON.parse(JSON.stringify(obj));
}else if(type==2){
// 初始化result
let result=null;
if(obj instanceof Array){
// 如果是数组
result=[];
}else{
// 不是数组那就是对象了
result={};
}
for(let key in obj){
// 判断key是否为obj自身属性值
if(obj.hasOwnProperty(key)){
result[key]=deepClone(obj[key],2);
}
}
return result;
}
}
let a = {
name:'小明',
age:15,
address:"九龙广场",
fun:["零食","羽毛球","篮球"],
parent:{
father:{
name:"小明爸爸",
age:32
},
mother:{
name:"小明妈妈",
age:34
}
}
}
let b = a;
// 查看当前a和b的原始数据
console.log('a',a);
// 使用json方式克隆
let c = deepClone(a,2);
a.name="小红";
console.log('a',a)
console.log('c',c)
console.log('修改a的父母信息');
a.parent.father="小红爸爸";
a.parent.mother="小红妈妈";
console.log('查看b和c')
console.log('b',b)
console.log('c',c)
简单的jquery, 考虑插件和扩展性
class Jquery{
constructor(selector){
let result=null;
if(selector.startsWith('.')){
result=document.getElementsByClassName(selector.slice(1))
}else{
result = document.querySelectorAll(selector);
}
let length = result.length;
for(let i = 0;i<length;i++){
this[i]=result[i];
}
this.length=length;
this.selector=selector;
}
get(index){
return this[index];
}
each(fun){
for(let i=0;i<this.length;i++){
fun(this[i]);
}
}
on(type,fun){
return this.each(elem=>{
elem.addEventListener(type,fun,false);
})
}
}
深拷贝( 破解递归爆栈 )
其实破解递归爆栈的方法有两条路,第一种是消除尾递归,但在这个例子中貌似行不通,第二种方法就是干脆不用递归,改用循环
用循环遍历一棵树,需要借助一个栈,当栈为空时就遍历完了,栈里面存储下一个需要拷贝的节点
首先我们往栈里放入种子数据,
key
用来存储放哪一个父元素的那一个子元素拷贝对象然后遍历当前节点下的子元素,如果是对象就放到栈里,否则直接拷贝
function cloneLoop(x) {
const root = {};
// 栈
const loopList = [
{
parent: root,
key: undefined,
data: x,
}
];
while(loopList.length) {
// 深度优先
const node = loopList.pop();
const parent = node.parent;
const key = node.key;
const data = node.data;
// 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
let res = parent;
if (typeof key !== 'undefined') {
res = parent[key] = {};
}
for(let k in data) {
if (data.hasOwnProperty(k)) {
if (typeof data[k] === 'object') {
// 下一次循环
loopList.push({
parent: res,
key: k,
data: data[k],
});
} else {
res[k] = data[k];
}
}
}
}
return root;
}
深拷贝( 破解循环引用 )
引入一个数组
uniqueList
用来存储已经拷贝的数组,每次循环遍历时,先判断对象是否在uniqueList
中了,如果在的话就不执行拷贝逻辑了
find
是抽象的一个函数,其实就是遍历uniqueList
// 保持引用关系
function cloneForce(x) {
// =============
const uniqueList = []; // 用来去重
// =============
let root = {};
// 循环数组
const loopList = [
{
parent: root,
key: undefined,
data: x,
}
];
while(loopList.length) {
// 深度优先
const node = loopList.pop();
const parent = node.parent;
const key = node.key;
const data = node.data;
// 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
let res = parent;
if (typeof key !== 'undefined') {
res = parent[key] = {};
}
// =============
// 数据已经存在
let uniqueData = find(uniqueList, data);
if (uniqueData) {
parent[key] = uniqueData.target;
continue; // 中断本次循环
}
// 数据不存在
// 保存源数据,在拷贝数据中对应的引用
uniqueList.push({
source: data,
target: res,
});
// =============
for(let k in data) {
if (data.hasOwnProperty(k)) {
if (typeof data[k] === 'object') {
// 下一次循环
loopList.push({
parent: res,
key: k,
data: data[k],
});
} else {
res[k] = data[k];
}
}
}
}
return root;
}
function find(arr, item) {
for(let i = 0; i < arr.length; i++) {
if (arr[i].source === item) {
return arr[i];
}
}
return null;
}
// 测试
var b = {};
var a = {a1: b, a2: b};
a.a1 === a.a2 // true
var c = cloneForce(a);
c.a1 === c.a2 // true
使用setTimeout模拟setInterval
function mySetInterval(cb,t){
setTimeout (function () {
cb()
setTimeout (arguments.callee, t)
}, t)
}
// return timer 使得定时器是可以取消的
function mySetInterval(cb,t){
return setTimeout (function () {
cb()
setTimeout (arguments.callee, t)
}, t)
}
js实现一个继承方法
// 借用构造函数继承实例属性
function Child () {
Parent.call(this)
}
// 寄生继承原型属性
(function () {
let Super = function () {}
Super.prototype = Parent.prototype
Child.prototype = new Super()
})()
手写ajax
//1.简单流程
//实例化
let xhr = XMLHttpRequest()
//初始化
xhr.open(method,url,async)
//发送请求
xhr.send(data)
//设置状态变化回调处理请求结果
xhr.onreadystatechange = ()=>{
if(xhr.readyState === 4 && xhr.state === 200){
console.log(xhr.responseText)
}
}
//2.基于promise实现
function ajax(options) {
//请求地址
const url = options.url,
//请求方法
const method = options.method.toLocaleLowerCase() || 'get'
//默认为异步
const async = options.async
//请求参数
const data = options.data
//实例化
const xhr = new XMLHtmlRequset()
//请求超时
if(options.timeout && options.timeout > 0) {
xhr.timeout = options.timeout
}
//返回一个promise实例
xhr.ontimeout = ()=> reject && reject('请求超时')
//监听状态变化回调
xhr.onreadystatechange = ()=> {
if(xhr.readyState == 4){
//200--300 表示请求成功 304 资源未变 取缓存
if(xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){
resolve && resolve(xhr.responseText)
}else{
reject && reject()
}
}
}
// 错误回调
xhr.onerror = err => reject && reject(err)
let paramArr = []
let encodeData
//处理请求参数 {a:1,b:'1212'} 处理成 a=1&b=1212
if(data instanceof Object){
for(let key in data){
//参数拼接需要通过 encodeURIComponent 进行编码
paramArr.push(encodeURIComponent(key)+'='+encodeURIComponent(data[key]))
}
encodeData = paramArr.join('&')
}
if(method === 'get'){
//检测url中是否已存在 '?' 及其位置
}
//初始化
xhr.open(method,url,async)
//发送请求
if(method === 'get') xhr.send(null)
else{
//post需要设置请求头
xhr.setRequestHeader('Content-Type','appliction/x-www-form-urlencoded;charset=UTF-8')
xhr.send(encodeData)
}
}
http请求封装
使用
XMLHttpRequest
和fetch
等原生方法Content-Type 类型
application/x-www-form-urlencoded
纯文本传输multipart/form-data
可用于上传文件
const baseUrl = 'http://localhost:3000'
export default async(url = '', data = {}, type = 'GET', method = 'fetch') => {
type = type.toUpperCase();
url = baseUrl + url;
if (type == 'GET') {
let dataStr = ''; //数据拼接字符串
Object.keys(data).forEach(key => {
dataStr += key + '=' + data[key] + '&';
})
if (dataStr !== '') {
dataStr = dataStr.substr(0, dataStr.lastIndexOf('&'));
url = url + '?' + dataStr;
}
}
if (window.fetch && method == 'fetch') {
let requestConfig = {
credentials: 'include',
method: type,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
mode: "cors",
cache: "force-cache"
}
if (type == 'POST') {
Object.defineProperty(requestConfig, 'body', {
value: JSON.stringify(data)
})
}
try {
const response = await fetch(url, requestConfig);
const responseJson = await response.json();
return responseJson
} catch (error) {
throw new Error(error)
}
} else {
return new Promise((resolve, reject) => {
let requestObj;
if (window.XMLHttpRequest) {
requestObj = new XMLHttpRequest();
} else {
requestObj = new ActiveXObject;
}
let sendData = '';
if (type == 'POST') {
sendData = JSON.stringify(data);
}
requestObj.open(type, url, true);
requestObj.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
requestObj.send(sendData);
requestObj.onreadystatechange = () => {
if (requestObj.readyState == 4) {
if (requestObj.status == 200) {
let obj = requestObj.response
if (typeof obj !== 'object') {
obj = JSON.parse(obj);
}
resolve(obj)
} else {
reject(requestObj)
}
}
}
})
}
}
注意一下 vue 中 mixins 的优先级,component > mixins > extends。
export function mergeOptions(
parent: Object,
child: Object,
vm?: Component
): Object {
...
// 如果有 child.extends 递归调用 mergeOptions 实现属性拷贝
const extendsFrom = child.extends
if (extendsFrom) {
parent = mergeOptions(parent, extendsFrom, vm)
}
// 如果有 child.mixins 递归调用 mergeOptions 实现属性拷贝
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
// 申明 options 空对象,用来保存属性拷贝结果
const options = {}
let key
// 遍历 parent 对象,调用 mergeField 进行属性拷贝
for (key in parent) {
mergeField(key)
}
// 遍历 parent 对象,调用 mergeField 进行属性拷贝
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
// 属性拷贝实现方法
function mergeField(key) {
// 穿透赋值,默认为 defaultStrat
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
介绍下深度优先遍历和广度优先遍历,如何实现?
深度优先遍历——是指从某个顶点出发,首先访问这个顶点,然后找出刚访问这个结点的第一个未被访问的邻结点,然后再以此邻结点为顶点,继续找它的下一个顶点进行访问。重复此步骤,直至所有结点都被访问完为止。
广度优先遍历——是从某个顶点出发,首先访问这个顶点,然后找出刚访问这个结点所有未被访问的邻结点,访问完后再访问这些结点中第一个邻结点的所有结点,重复此方法,直到所有结点都被访问完为止。
```js
//1.深度优先遍历的递归写法
function deepTraversal(node) {
let nodes = []
if (node != null) {
nodes.push[node]
let childrens = node.children
for (let i = 0; i < childrens.length; i++)
deepTraversal(childrens[i])
}
return nodes
}
//2.深度优先遍历的非递归写法
function deepTraversal(node) {
let nodes = []
if (node != null) {
let stack = [] //同来存放将来要访问的节点
stack.push(node)
while (stack.length != 0) {
let item = stack.pop() //正在访问的节点
nodes.push(item)
let childrens = item.children
for (
let i = childrens.length - 1;
i >= 0;
i-- //将现在访问点的节点的子节点存入stack,供将来访问
)
stack.push(childrens[i])
}
}
return nodes
}
//3.广度优先遍历的递归写法
function wideTraversal(node) {
let nodes = [],
i = 0
if (node != null) {
nodes.push(node)
wideTraversal(node.nextElementSibling)
node = nodes[i++]
wideTraversal(node.firstElementChild)
}
return nodes
}
//4.广度优先遍历的非递归写法
function wideTraversal(node) {
let nodes = [],
i = 0
while (node != null) {
nodes.push(node)
node = nodes[i++]
let childrens = node.children
for (let i = 0; i < childrens.length; i++) {
nodes.push(childrens[i])
}
}
return nodes
}
<a name="g74YS"></a>
#### 使用Promise封装AJAX请求
```javascript
// promise 封装实现:
function getJSON(url) {
// 创建一个 promise 对象
let promise = new Promise(function(resolve, reject) {
let xhr = new XMLHttpRequest();
// 新建一个 http 请求
xhr.open("GET", url, true);
// 设置状态的监听函数
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 当请求成功或失败时,改变 promise 的状态
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
// 设置错误监听函数
xhr.onerror = function() {
reject(new Error(this.statusText));
};
// 设置响应的数据类型
xhr.responseType = "json";
// 设置请求头信息
xhr.setRequestHeader("Accept", "application/json");
// 发送 http 请求
xhr.send(null); // 参数data为携带的数据
});
return promise;
}
实现sleep函数(使用Promise封装setTimeout)
function timeout(delay) {
return new Promise(resolve => {
setTimeout(resolve, delay)
})
};
使用 setTimeout 实现 setInterval
function mySetInterval(fn, timeout) {
// 控制器,控制定时器是否继续执行
var timer = {
flag: true
};
// 设置递归函数,模拟定时器执行。
function interval() {
if (timer.flag) {
fn();
setTimeout(interval, timeout);
}
}
// 启动定时器
setTimeout(interval, timeout);
// 返回控制器
return timer;
}
判断对象是否存在循环引用
循环引用对象本来没有什么问题,但是序列化的时候就会发生问题,比如调用
JSON.stringify()
对该类对象进行序列化,就会报错:Converting circular structure to JSON.
const isCycleObject = (obj,parent) => {
const parentArr = parent || [obj];
for(let i in obj) {
if(typeof obj[i] === 'object') {
let flag = false;
parentArr.forEach((pObj) => {
if(pObj === obj[i]){
flag = true;
}
})
if(flag) return true;
flag = isCycleObject(obj[i],[...parentArr,obj[i]]);
if(flag) return true;
}
}
return false;
}
const a = 1;
const b = {a};
const c = {b};
const o = {d:{a:3},c}
o.c.b.aa = a;
console.log(isCycleObject(o))
手写一个观察者模式
class Subject{ // 被观察者的类 被观察者 需要将观察者收集起来
constructor(name){
this.name = name;
this.state = '非常开心'
this.observers = [];
}
attach(o){ // 进行收集
this.observers.push(o); // on
}
setState(newState){
this.state = newState;
this.observers.forEach(o=>o.update(this.name,newState)) // emit
}
}
class Observer{ // 观察者
constructor(name){
this.name = name;
}
update(s,state){
console.log(this.name+":" + s + '当前'+state);
}
}
// vue 数据变了(状态) 视图要更新 (通知依赖的人)
let s = new Subject('小宝宝');
let o1 = new Observer('爸爸');
let o2 = new Observer('妈妈');
s.attach(o1)
s.attach(o2)
s.setState('不开心了')
s.setState('开心了')
手写一个观察者模式
class Subject{ // 被观察者的类 被观察者 需要将观察者收集起来
constructor(name){
this.name = name;
this.state = '非常开心'
this.observers = [];
}
attach(o){ // 进行收集
this.observers.push(o); // on
}
setState(newState){
this.state = newState;
this.observers.forEach(o=>o.update(this.name,newState)) // emit
}
}
class Observer{ // 观察者
constructor(name){
this.name = name;
}
update(s,state){
console.log(this.name+":" + s + '当前'+state);
}
}
// vue 数据变了(状态) 视图要更新 (通知依赖的人)
let s = new Subject('小宝宝');
let o1 = new Observer('爸爸');
let o2 = new Observer('妈妈');
s.attach(o1)
s.attach(o2)
s.setState('不开心了')
s.setState('开心了')
手写去重
使用闭包实现每隔一秒打印 1,2,3,4
// 使用闭包实现
for (var i = 0; i < 5; i++) {
(function(i) {
setTimeout(function() {
console.log(i);
}, i * 1000);
})(i);
}
// 使用 let 块级作用域
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}
手写一个 jsonp
function jsonp(url, params, callback) {
// 判断是否含有参数
let queryString = url.indexOf("?") === -1 ? "?" : "&";
// 添加参数
for (var k in params) {
if (params.hasOwnProperty(k)) {
queryString += k + "=" + params[k] + "&";
}
}
// 处理回调函数名
let random = Math.random()
.toString
.replace(".", ""),
callbackName = "myJsonp" + random;
// 添加回调函数
queryString += "callback=" + callbackName;
// 构建请求
let scriptNode = document.createElement("script");
scriptNode.src = url + queryString;
window[callbackName] = function() {
// 调用回调函数
callback(...arguments);
// 删除这个引入的脚本
document.getElementsByTagName("head")[0].removeChild(scriptNode);
};
// 发起请求
document.getElementsByTagName("head")[0].appendChild(scriptNode);
}
详细资料可以参考:《原生 jsonp 具体实现》《jsonp 的原理与实现》
实现 compose 函数
compose(组合)函数特点:
- compose 的参数是函数,返回的也是一个函数
- 因为除了第一个函数的接受参数,其他函数的接受参数都是上一个函数的返回值,所以初始函数的参数是多元的,而其他函数的接受值是一元的
- compsoe 函数可以接受任意的参数,所有的参数都是函数,且执行方向是自右向左的,初始函数一定放到参数的最右面
实现
```js
function compose(...fns) {
let isFirst = true
return (...args) => {
return fns.reduceRight((result, fn) => {
if (!isFirst) return fn(result)
isFirst = false
return fn(...result)
}, args)
}
}
测试
const greeting = (firstName, lastName) => 'hello, ' + firstName + ' ' + lastName
const toUpper = str => str.toUpperCase()
const fn = compose(toUpper, greeting)
console.log(fn('jack', 'smith')) // HELLO, JACK SMITH
<a name="FaW6h"></a>
#### 格式化金钱,每千分位加逗号
```javascript
// 1
function format(str) {
let s = ''
let count = 0
for (let i = str.length - 1; i >= 0; i--) {
s = str[i] + s
count++
if (count % 3 == 0 && i != 0) {
s = ',' + s
}
}
return s
}
// 2
function format(str) {
return str.replace(/(\d)(?=(?:\d{3})+$)/g, '$1,')
}
请用js去除字符串空格
## 去除所有空格
str.replace(/\s/g, '')
## 去除两边空格
str.replace(/^\s+|\s+$/g, '')
// 原生方法
str.trim()
反转数组
要求
input: I am a student output: student a am I 输入是数组 输出也是数组不允许用 split splice reverse
// 1
function reverseArry(arr) {
const result = []
while (arr.length) {
result.push(arr.pop())
}
return result
}
console.log(reverseArry(['I', 'am', 'a', 'student']))
// ["student", "a", "am", "I"]
// 2
function reverseArry(arry) {
const result = []
const distance = arry.length - 1
for (let i = distance; i >= 0; i--) {
result[distance - i] = arry[i]
}
return result
}
将金额12345转成中文金额表示
要求
12345 => 一万两千三百四十五 10086 => 一万零八十六 100010001 => 一亿零一万零一 100000000 => 一亿 单位支持到亿
function numToString(num) {
if (num > 999999999) throw '超过金额上限,最大单位为亿'
const unitMap = ['', '十', '百', '千', '万', '十', '百', '千', '亿']
const stringMap = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九']
const n = num + ''
const len = n.length
let lastIndex = len - 1
let result = ''
for (let i = 0; i < len; i++) {
if (i > 0 && n[i] == '0') {
if (n[i - 1] != '0') result += '零'
} else {
result += stringMap[n[i]] + unitMap[lastIndex]
}
lastIndex--
}
lastIndex = result.length - 1
if (result[lastIndex] == '零') return result.slice(0, lastIndex)
return result
}
console.log(numToString(12345)) // 一万二千三百四十五
console.log(numToString(10086)) // 一万零八十六
console.log(numToString(100010001)) // 一亿零一万零一
console.log(numToString(100000000)) // 一亿
异步求和
要求
提供一个异步 add 方法如下,需要实现一个 await sum(…args) 函数: function asyncAdd(a, b, callback) { setTimeout(function() { callback(null, a + b) }, 1000) }
function sum(...args) {
let start = 0
let result = 0
let count = 0 // 用于计算开启了多少个 Promise
function _sum(resolve) {
count++
new Promise((r, j) => {
let a = args[start++]
let b = args[start++]
a = a !== undefined? a : 0
b = b !== undefined? b : 0 // 如果访问的元素超出了数组范围,则转为 0
asyncAdd(a, b, (context, sum) => {
r(sum)
})
if (start < args.length) {
_sum(resolve)
}
})
.then(sum => {
result += sum
count--
if (count == 0) resolve(result) // 所有的 Promise 执行完毕,返回结果
})
}
return new Promise((resolve, reject) => {
if (!args || !args.length) return resolve(0)
if (args.length == 1) return resolve(args[0])
_sum(resolve)
})
}
// 测试
sum(1,2,3,4,5,6,7,8,9,10,11).then(sum => console.log(sum)) // 66
// or
async function test() {
console.log(await sum(1,2,3,4,5,6,7,8,9,10,11))
}
test() // 66
数字集转换成字母集
要求
a~z 有 26个字母,按照 1~26 编码,现在给定一个数字字符串,输出所有可能的解码结果,如:输入 1234,输出 [‘awd’, ‘abcd’, ‘lcd’]
const map = [0,'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z']
function getDecodes(num) {
if (!num) return []
num += ''
const result = []
_getDecodes(num, 0, [], result)
return result
}
function _getDecodes(num, start, path, result) {
if (start == num.length) return result.push([...path])
let c = num[start++]
path.push(map[c])
_getDecodes(num, start, path, result)
path.pop()
if (start == num.length) return
c += num[start]
if (c > 26) return
path.push(map[c])
_getDecodes(num, start + 1, path, result)
path.pop()
}