共计 15443 个字符,预计需要花费 39 分钟才能阅读完成。
解析 URL Params 为对象
let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';
parseParam(url)
/* 后果
{ user: 'anonymous',
id: [123, 456], // 反复呈现的 key 要组装成数组,能被转成数字的就转成数字类型
city: '北京', // 中文需解码
enabled: true, // 未指定值得 key 约定为 true
}
*/
function parseParam(url) {const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 前面的字符串取出来
const paramsArr = paramsStr.split('&'); // 将字符串以 & 宰割后存到数组中
let paramsObj = {};
// 将 params 存到对象中
paramsArr.forEach(param => {if (/=/.test(param)) { // 解决有 value 的参数
let [key, val] = param.split('='); // 宰割 key 和 value
val = decodeURIComponent(val); // 解码
val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字
if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则增加一个值
paramsObj[key] = [].concat(paramsObj[key], val);
} else { // 如果对象没有这个 key,创立 key 并设置值
paramsObj[key] = val;
}
} else { // 解决没有 value 的参数
paramsObj[param] = true;
}
})
return paramsObj;
}
实现 apply 办法
apply 原理与 call 很类似,不多赘述
// 模仿 apply
Function.prototype.myapply = function(context, arr) {var context = Object(context) || window;
context.fn = this;
var result;
if (!arr) {result = context.fn();
} else {var args = [];
for (var i = 0, len = arr.length; i < len; i++) {args.push("arr[" + i + "]");
}
result = eval("context.fn(" + args + ")");
}
delete context.fn;
return result;
};
Array.prototype.map()
Array.prototype.map = function(callback, thisArg) {if (this == undefined) {throw new TypeError('this is null or not defined');
}
if (typeof callback !== 'function') {throw new TypeError(callback + 'is not a function');
}
const res = [];
// 同理
const O = Object(this);
const len = O.length >>> 0;
for (let i = 0; i < len; i++) {if (i in O) {
// 调用回调函数并传入新数组
res[i] = callback.call(thisArg, O[i], i, this);
}
}
return res;
}
实现 Event(event bus)
event bus 既是 node 中各个模块的基石,又是前端组件通信的依赖伎俩之一,同时波及了订阅 - 公布设计模式,是十分重要的根底。
简略版:
class EventEmeitter {constructor() {this._events = this._events || new Map(); // 贮存事件 / 回调键值对
this._maxListeners = this._maxListeners || 10; // 设立监听下限
}
}
// 触发名为 type 的事件
EventEmeitter.prototype.emit = function(type, ...args) {
let handler;
// 从贮存事件键值对的 this._events 中获取对应事件回调函数
handler = this._events.get(type);
if (args.length > 0) {handler.apply(this, args);
} else {handler.call(this);
}
return true;
};
// 监听名为 type 的事件
EventEmeitter.prototype.addListener = function(type, fn) {
// 将 type 事件以及对应的 fn 函数放入 this._events 中贮存
if (!this._events.get(type)) {this._events.set(type, fn);
}
};
面试版:
class EventEmeitter {constructor() {this._events = this._events || new Map(); // 贮存事件 / 回调键值对
this._maxListeners = this._maxListeners || 10; // 设立监听下限
}
}
// 触发名为 type 的事件
EventEmeitter.prototype.emit = function(type, ...args) {
let handler;
// 从贮存事件键值对的 this._events 中获取对应事件回调函数
handler = this._events.get(type);
if (args.length > 0) {handler.apply(this, args);
} else {handler.call(this);
}
return true;
};
// 监听名为 type 的事件
EventEmeitter.prototype.addListener = function(type, fn) {
// 将 type 事件以及对应的 fn 函数放入 this._events 中贮存
if (!this._events.get(type)) {this._events.set(type, fn);
}
};
// 触发名为 type 的事件
EventEmeitter.prototype.emit = function(type, ...args) {
let handler;
handler = this._events.get(type);
if (Array.isArray(handler)) {
// 如果是一个数组阐明有多个监听者, 须要顺次此触发外面的函数
for (let i = 0; i < handler.length; i++) {if (args.length > 0) {handler[i].apply(this, args);
} else {handler[i].call(this);
}
}
} else {
// 单个函数的状况咱们间接触发即可
if (args.length > 0) {handler.apply(this, args);
} else {handler.call(this);
}
}
return true;
};
// 监听名为 type 的事件
EventEmeitter.prototype.addListener = function(type, fn) {const handler = this._events.get(type); // 获取对应事件名称的函数清单
if (!handler) {this._events.set(type, fn);
} else if (handler && typeof handler === "function") {
// 如果 handler 是函数阐明只有一个监听者
this._events.set(type, [handler, fn]); // 多个监听者咱们须要用数组贮存
} else {handler.push(fn); // 曾经有多个监听者, 那么间接往数组里 push 函数即可
}
};
EventEmeitter.prototype.removeListener = function(type, fn) {const handler = this._events.get(type); // 获取对应事件名称的函数清单
// 如果是函数, 阐明只被监听了一次
if (handler && typeof handler === "function") {this._events.delete(type, fn);
} else {
let postion;
// 如果 handler 是数组, 阐明被监听屡次要找到对应的函数
for (let i = 0; i < handler.length; i++) {if (handler[i] === fn) {postion = i;} else {postion = -1;}
}
// 如果找到匹配的函数, 从数组中革除
if (postion !== -1) {
// 找到数组对应的地位, 间接革除此回调
handler.splice(postion, 1);
// 如果革除后只有一个函数, 那么勾销数组, 以函数模式保留
if (handler.length === 1) {this._events.set(type, handler[0]);
}
} else {return this;}
}
};
实现具体过程和思路见实现 event
判断是否是电话号码
function isPhone(tel) {var regx = /^1[34578]\d{9}$/;
return regx.test(tel);
}
实现 AJAX 申请
AJAX 是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新以后网页的对应局部,而不必刷新整个网页。
创立 AJAX 申请的步骤:
- 创立一个 XMLHttpRequest 对象。
- 在这个对象上 应用 open 办法创立一个 HTTP 申请,open 办法所须要的参数是申请的办法、申请的地址、是否异步和用户的认证信息。
- 在发动申请前,能够为这个对象 增加一些信息和监听函数。比如说能够通过 setRequestHeader 办法来为申请增加头信息。还能够为这个对象增加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变动时会触发 onreadystatechange 事件,能够通过设置监听函数,来解决申请胜利后的后果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接管实现,这个时候能够通过判断申请的状态,如果状态是 2xx 或者 304 的话则代表返回失常。这个时候就能够通过 response 中的数据来对页面进行更新了。
- 当对象的属性和监听函数设置实现后,最初调 用 sent 办法来向服务器发动申请,能够传入参数作为发送的数据体。
const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创立 Http 申请
xhr.open("GET", SERVER_URL, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {if (this.readyState !== 4) return;
// 当申请胜利时
if (this.status === 200) {handle(this.response);
} else {console.error(this.statusText);
}
};
// 设置申请失败时的监听函数
xhr.onerror = function() {console.error(this.statusText);
};
// 设置申请头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 发送 Http 申请
xhr.send(null);
参考 前端进阶面试题具体解答
Array.prototype.forEach()
Array.prototype.forEach = function(callback, thisArg) {if (this == null) {throw new TypeError('this is null or not defined');
}
if (typeof callback !== "function") {throw new TypeError(callback + 'is not a function');
}
const O = Object(this);
const len = O.length >>> 0;
let k = 0;
while (k < len) {if (k in O) {callback.call(thisArg, O[k], k, O);
}
k++;
}
}
Object.assign
Object.assign()
办法用于将所有可枚举属性的值从一个或多个源对象复制到指标对象。它将返回指标对象(请留神这个操作是浅拷贝)
Object.defineProperty(Object, 'assign', {value: function(target, ...args) {if (target == null) {return new TypeError('Cannot convert undefined or null to object');
}
// 指标对象须要对立是援用数据类型,若不是会主动转换
const to = Object(target);
for (let i = 0; i < args.length; i++) {
// 每一个源对象
const nextSource = args[i];
if (nextSource !== null) {
// 应用 for...in 和 hasOwnProperty 双重判断,确保只拿到自身的属性、办法(不蕴含继承的)for (const nextKey in nextSource) {if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
},
// 不可枚举
enumerable: false,
writable: true,
configurable: true,
})
实现字符串的 repeat 办法
输出字符串 s,以及其反复的次数,输入反复的后果,例如输出 abc,2,输入 abcabc。
function repeat(s, n) {return (new Array(n + 1)).join(s);
}
递归:
function repeat(s, n) {return (n > 0) ? s.concat(repeat(s, --n)) : "";
}
实现浅拷贝
浅拷贝是指,一个新的对象对原始对象的属性值进行准确地拷贝,如果拷贝的是根本数据类型,拷贝的就是根本数据类型的值,如果是援用数据类型,拷贝的就是内存地址。如果其中一个对象的援用内存地址产生扭转,另一个对象也会发生变化。
(1)Object.assign()
Object.assign()
是 ES6 中对象的拷贝办法,承受的第一个参数是指标对象,其余参数是源对象,用法:Object.assign(target, source_1, ···)
,该办法能够实现浅拷贝,也能够实现一维对象的深拷贝。
留神:
- 如果指标对象和源对象有同名属性,或者多个源对象有同名属性,则前面的属性会笼罩后面的属性。
- 如果该函数只有一个参数,当参数为对象时,间接返回该对象;当参数不是对象时,会先将参数转为对象而后返回。
- 因为
null
和undefined
不能转化为对象,所以第一个参数不能为null
或undefined
,会报错。
let target = {a: 1};
let object2 = {b: 2};
let object3 = {c: 3};
Object.assign(target,object2,object3);
console.log(target); // {a: 1, b: 2, c: 3}
(2)扩大运算符
应用扩大运算符能够在结构字面量对象的时候,进行属性的拷贝。语法:let cloneObj = {...obj};
let obj1 = {a:1,b:{c:1}}
let obj2 = {...obj1};
obj1.a = 2;
console.log(obj1); //{a:2,b:{c:1}}
console.log(obj2); //{a:1,b:{c:1}}
obj1.b.c = 2;
console.log(obj1); //{a:2,b:{c:2}}
console.log(obj2); //{a:1,b:{c:2}}
(3)数组办法实现数组浅拷贝
1)Array.prototype.slice
slice()
办法是 JavaScript 数组的一个办法,这个办法能够从已有数组中返回选定的元素:用法:array.slice(start, end)
,该办法不会扭转原始数组。- 该办法有两个参数,两个参数都可选,如果两个参数都不写,就能够实现一个数组的浅拷贝。
let arr = [1,2,3,4];
console.log(arr.slice()); // [1,2,3,4]
console.log(arr.slice() === arr); //false
2)Array.prototype.concat
concat()
办法用于合并两个或多个数组。此办法不会更改现有数组,而是返回一个新数组。- 该办法有两个参数,两个参数都可选,如果两个参数都不写,就能够实现一个数组的浅拷贝。
let arr = [1,2,3,4];
console.log(arr.concat()); // [1,2,3,4]
console.log(arr.concat() === arr); //false
(4)手写实现浅拷贝
// 浅拷贝的实现;
function shallowCopy(object) {
// 只拷贝对象
if (!object || typeof object !== "object") return;
// 依据 object 的类型判断是新建一个数组还是对象
let newObject = Array.isArray(object) ? [] : {};
// 遍历 object,并且判断是 object 的属性才拷贝
for (let key in object) {if (object.hasOwnProperty(key)) {newObject[key] = object[key];
}
}
return newObject;
}// 浅拷贝的实现;
function shallowCopy(object) {
// 只拷贝对象
if (!object || typeof object !== "object") return;
// 依据 object 的类型判断是新建一个数组还是对象
let newObject = Array.isArray(object) ? [] : {};
// 遍历 object,并且判断是 object 的属性才拷贝
for (let key in object) {if (object.hasOwnProperty(key)) {newObject[key] = object[key];
}
}
return newObject;
}// 浅拷贝的实现;
function shallowCopy(object) {
// 只拷贝对象
if (!object || typeof object !== "object") return;
// 依据 object 的类型判断是新建一个数组还是对象
let newObject = Array.isArray(object) ? [] : {};
// 遍历 object,并且判断是 object 的属性才拷贝
for (let key in object) {if (object.hasOwnProperty(key)) {newObject[key] = object[key];
}
}
return newObject;
}
字符串解析问题
var a = {
b: 123,
c: '456',
e: '789',
}
var str=`a{a.b}aa{a.c}aa {a.d}aaaa`;
// => 'a123aa456aa {a.d}aaaa'
实现函数使得将 str 字符串中的 {}
内的变量替换,如果属性不存在放弃原样(比方{a.d}
)
相似于模版字符串,但有一点出入,实际上原理大差不差
const fn1 = (str, obj) => {
let res = '';
// 标记位,标记后面是否有{
let flag = false;
let start;
for (let i = 0; i < str.length; i++) {if (str[i] === '{') {
flag = true;
start = i + 1;
continue;
}
if (!flag) res += str[i];
else {if (str[i] === '}') {
flag = false;
res += match(str.slice(start, i), obj);
}
}
}
return res;
}
// 对象匹配操作
const match = (str, obj) => {const keys = str.split('.').slice(1);
let index = 0;
let o = obj;
while (index < keys.length) {const key = keys[index];
if (!o[key]) {return `{${str}}`;
} else {o = o[key];
}
index++;
}
return o;
}
将 VirtualDom 转化为实在 DOM 构造
这是以后 SPA 利用的外围概念之一
// vnode 构造:// {
// tag,
// attrs,
// children,
// }
//Virtual DOM => DOM
function render(vnode, container) {container.appendChild(_render(vnode));
}
function _render(vnode) {
// 如果是数字类型转化为字符串
if (typeof vnode === 'number') {vnode = String(vnode);
}
// 字符串类型间接就是文本节点
if (typeof vnode === 'string') {return document.createTextNode(vnode);
}
// 一般 DOM
const dom = document.createElement(vnode.tag);
if (vnode.attrs) {
// 遍历属性
Object.keys(vnode.attrs).forEach(key => {const value = vnode.attrs[key];
dom.setAttribute(key, value);
})
}
// 子数组进行递归操作
vnode.children.forEach(child => render(child, dom));
return dom;
}
实现 add(1)(2)(3)
函数柯里化概念:柯里化(Currying)是把承受多个参数的函数转变为承受一个繁多参数的函数,并且返回承受余下的参数且返回后果的新函数的技术。
1)粗犷版
function add (a) {return function (b) {return function (c) {return a + b + c;}
}
}
console.log(add(1)(2)(3)); // 6
2)柯里化解决方案
- 参数长度固定
var add = function (m) {var temp = function (n) {return add(m + n);
}
temp.toString = function () {return m;}
return temp;
};
console.log(add(3)(4)(5)); // 12
console.log(add(3)(6)(9)(25)); // 43
对于 add(3)(4)(5),其执行过程如下:
- 先执行 add(3),此时 m =3,并且返回 temp 函数;
- 执行 temp(4),这个函数内执行 add(m+n),n 是此次传进来的数值 4,m 值还是上一步中的 3,所以 add(m+n)=add(3+4)=add(7),此时 m =7,并且返回 temp 函数
- 执行 temp(5),这个函数内执行 add(m+n),n 是此次传进来的数值 5,m 值还是上一步中的 7,所以 add(m+n)=add(7+5)=add(12),此时 m =12,并且返回 temp 函数
- 因为前面没有传入参数,等于返回的 temp 函数不被执行而是打印,理解 JS 的敌人都晓得对象的 toString 是批改对象转换字符串的办法,因而代码中 temp 函数的 toString 函数 return m 值,而 m 值是最初一步执行函数时的值 m =12,所以返回值是 12。
- 参数长度不固定
function add (...args) {
// 求和
return args.reduce((a, b) => a + b)
}
function currying (fn) {let args = []
return function temp (...newArgs) {if (newArgs.length) {
args = [
...args,
...newArgs
]
return temp
} else {let val = fn.apply(this, args)
args = [] // 保障再次调用时清空
return val
}
}
}
let addCurry = currying(add)
console.log(addCurry(1)(2)(3)(4, 5)()) //15
console.log(addCurry(1)(2)(3, 4, 5)()) //15
console.log(addCurry(1)(2, 3, 4, 5)()) //15
应用 ES5 和 ES6 求函数参数的和
ES5:
function sum() {
let sum = 0
Array.prototype.forEach.call(arguments, function(item) {sum += item * 1})
return sum
}
ES6:
function sum(...nums) {
let sum = 0
nums.forEach(function(item) {sum += item * 1})
return sum
}
Promise.race
Promise.race = function(promiseArr) {return new Promise((resolve, reject) => {
promiseArr.forEach(p => {
// 如果不是 Promise 实例须要转化为 Promise 实例
Promise.resolve(p).then(val => resolve(val),
err => reject(err),
)
})
})
}
Promise
// 模仿实现 Promise
// Promise 利用三大伎俩解决回调天堂:// 1. 回调函数提早绑定
// 2. 返回值穿透
// 3. 谬误冒泡
// 定义三种状态
const PENDING = 'PENDING'; // 进行中
const FULFILLED = 'FULFILLED'; // 已胜利
const REJECTED = 'REJECTED'; // 已失败
class Promise {constructor(exector) {
// 初始化状态
this.status = PENDING;
// 将胜利、失败后果放在 this 上,便于 then、catch 拜访
this.value = undefined;
this.reason = undefined;
// 胜利态回调函数队列
this.onFulfilledCallbacks = [];
// 失败态回调函数队列
this.onRejectedCallbacks = [];
const resolve = value => {
// 只有进行中状态能力更改状态
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
// 胜利态函数顺次执行
this.onFulfilledCallbacks.forEach(fn => fn(this.value));
}
}
const reject = reason => {
// 只有进行中状态能力更改状态
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
// 失败态函数顺次执行
this.onRejectedCallbacks.forEach(fn => fn(this.reason))
}
}
try {
// 立刻执行 executor
// 把外部的 resolve 和 reject 传入 executor,用户可调用 resolve 和 reject
exector(resolve, reject);
} catch(e) {
// executor 执行出错,将谬误内容 reject 抛出去
reject(e);
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function'? onRejected :
reason => {throw new Error(reason instanceof Error ? reason.message : reason) }
// 保留 this
const self = this;
return new Promise((resolve, reject) => {if (self.status === PENDING) {self.onFulfilledCallbacks.push(() => {
// try 捕捉谬误
try {
// 模仿微工作
setTimeout(() => {const result = onFulfilled(self.value);
// 分两种状况:// 1. 回调函数返回值是 Promise,执行 then 操作
// 2. 如果不是 Promise,调用新 Promise 的 resolve 函数
result instanceof Promise ? result.then(resolve, reject) : resolve(result);
})
} catch(e) {reject(e);
}
});
self.onRejectedCallbacks.push(() => {
// 以下同理
try {setTimeout(() => {const result = onRejected(self.reason);
// 不同点:此时是 reject
result instanceof Promise ? result.then(resolve, reject) : resolve(result);
})
} catch(e) {reject(e);
}
})
} else if (self.status === FULFILLED) {
try {setTimeout(() => {const result = onFulfilled(self.value);
result instanceof Promise ? result.then(resolve, reject) : resolve(result);
});
} catch(e) {reject(e);
}
} else if (self.status === REJECTED) {
try {setTimeout(() => {const result = onRejected(self.reason);
result instanceof Promise ? result.then(resolve, reject) : resolve(result);
})
} catch(e) {reject(e);
}
}
});
}
catch(onRejected) {return this.then(null, onRejected);
}
static resolve(value) {if (value instanceof Promise) {
// 如果是 Promise 实例,间接返回
return value;
} else {
// 如果不是 Promise 实例,返回一个新的 Promise 对象,状态为 FULFILLED
return new Promise((resolve, reject) => resolve(value));
}
}
static reject(reason) {return new Promise((resolve, reject) => {reject(reason);
})
}
static all(promiseArr) {
const len = promiseArr.length;
const values = new Array(len);
// 记录曾经胜利执行的 promise 个数
let count = 0;
return new Promise((resolve, reject) => {for (let i = 0; i < len; i++) {// Promise.resolve()解决,确保每一个都是 promise 实例
Promise.resolve(promiseArr[i]).then(
val => {values[i] = val;
count++;
// 如果全副执行完,返回 promise 的状态就能够扭转了
if (count === len) resolve(values);
},
err => reject(err),
);
}
})
}
static race(promiseArr) {return new Promise((resolve, reject) => {
promiseArr.forEach(p => {Promise.resolve(p).then(val => resolve(val),
err => reject(err),
)
})
})
}
}
手写 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;
};
实现 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});
查找文章中呈现频率最高的单词
function findMostWord(article) {
// 合法性判断
if (!article) return;
// 参数解决
article = article.trim().toLowerCase();
let wordList = article.match(/[a-z]+/g),
visited = [],
maxNum = 0,
maxWord = "";
article = "" + wordList.join(" ") +" ";
// 遍历判断单词呈现次数
wordList.forEach(function(item) {if (visited.indexOf(item) < 0) {
// 退出 visited
visited.push(item);
let word = new RegExp("" + item +" ","g"),
num = article.match(word).length;
if (num > maxNum) {
maxNum = num;
maxWord = item;
}
}
});
return maxWord + " " + maxNum;
}
应用 setTimeout 实现 setInterval
setInterval 的作用是每隔一段指定工夫执行一个函数,然而这个执行不是真的到了工夫立刻执行,它真正的作用是每隔一段时间将事件退出事件队列中去,只有当以后的执行栈为空的时候,能力去从事件队列中取出事件执行。所以可能会呈现这样的状况,就是以后执行栈执行的工夫很长,导致事件队列里边积攒多个定时器退出的事件,当执行栈完结的时候,这些事件会顺次执行,因而就不能到距离一段时间执行的成果。
针对 setInterval 的这个毛病,咱们能够应用 setTimeout 递归调用来模仿 setInterval,这样咱们就确保了只有一个事件完结了,咱们才会触发下一个定时器事件,这样解决了 setInterval 的问题。
实现思路是应用递归函数,一直地去执行 setTimeout 从而达到 setInterval 的成果
function mySetInterval(fn, timeout) {
// 控制器,管制定时器是否继续执行
var timer = {flag: true};
// 设置递归函数,模仿定时器执行。function interval() {if (timer.flag) {fn();
setTimeout(interval, timeout);
}
}
// 启动定时器
setTimeout(interval, timeout);
// 返回控制器
return timer;
}