前言
面试复习总结:多参照于冴羽的博客
1. 实现一个new操作符
- 首先函数接受不定量的参数,第一个参数为构造函数,接下来的参数被构造函数使用
- 然后内部创建一个空对象 obj
- 因为 obj 对象需要访问到构造函数原型链上的属性,所以我们通过 obj.proto = func.prototype 将两者联系起来。
- 通过call或apply,把构造函数的this指向obj
- 判断构造函数返回值是否为对象,如果为对象就使用构造函数返回的值,否则使用 obj,这样就实现了忽略构造函数返回的原始值
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.add = function() {
console.log(this.name)
console.log(this.age)
}
function objectFactory(func) {
var obj = {}
var arg = [...arguments].slice(1)
obj.__proto__ = func.prototype;
var res = func.apply(obj, arg)
if ((typeof res === 'Object' || typeof res === 'Function') && res !== null) {
return res
}
return obj
}
var person = objectFactory(Person, 'zh', 18)
2.实现一个call apply 或 bind
2.1 call
fun.call(thisArg, arg1, arg2, ...)
let obj = {
value: 1
}
function bar(name, age) {
console.log(this.value)
console.log(name)
console.log(age)
}
bar.call(obj, 'zh', 18)
注意:
- call改变了函数this指向, 指向bar
- 除了第一个参数, 其余参数一个个传入
- bar函数执行
可通过下面模拟第1,2步
- 将函数设为对象的属性
- 执行该函数
- 删除该函数
obj.fn = bar
obj.fn()
delete obj.fn()
完整版
Function.prototype.myCall = function(context) {
context = context || window
let args = [...arguments].slice(1)
context.fn = this
let res = context.fn(...args)
delete context.fn
return res
}
bar.myCall(obj, 'zh', 18)
2.2 apply
Function.prototype.myApply = function(context) {
let res
context = context || window
context.fn = this
if (arguments[1]) {
context.fn(args)
} else {
context.fn()
}
delete context.fn
return res
}
bar.myApply(obj, ['zh', 18])
2.3 bind
bind() 方法会创建一个新函数。当这个新函数被调用时,bind()的第一个参数将作为它运行时的 this,而其余参数将作为新函数的参数,供调用时使用
bind 函数算是手写部分比较难的一类,因为要考虑到绑定后返回的函数还能够作为构造函数被实例化
Function.prototype.myBind = function(context) {
if(typeof this != "function") {
throw Error("not a function")
}
let fn = this
let args = [...arguments].slice(1)
let res = function() {
//如果是构造函数,this (barbind 实例)指向 res(new barBind 构造函数), 此时结果为 true, 将绑定函数的 this 指向 this,
//如果是普通函数,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
return fn.apply(this instanceof res ? this : context,args.concat(...arguments) )
}
// res.prototype = this.prototype 修改函数原型时,构造函数的原型也会被同步修改了,可通过一个空函数来进行中转
function temp() {}
temp.prototype = this.prototype
res.prototype = new temp()
return res
}
var obj = {
value: 1
}
function Bar(name,age) {
console.log(this.value)
console.log(name, age)
this.name = name
this.age = age
}
Bar.prototype.add = function() {
console.log(this.name)
}
var barBind = Bar.myBind(obj)
var barbind = new barBind('zh', 18)
3. 手写防抖(Debouncing)和节流(Throttling)
3.1 防抖
事件持续触发,等到事件停止触发后n秒才去执行函数,如果n秒内又触发了该事件,那我就以新的事件的时间为准,n 秒后才执行。
function debounce(func, delay) {
let timeout;
return function () {
const context = this;
const args = arguments;
clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(context, args)
}, delay);
}
}
**新增需求:
1、事件触发,立刻执行函数,然后等到停止触发后 n 秒才执行函数。
2、事件再次触发,等到事件停止触发后n秒才去执行函数,
3、增加immediate 参数判断是否是立刻执行
function debounce(fn, delay, immediate=false){
let timeout = null;
function debounce(fn, delay, immediate = false) {
let timeout = null;
return function (args) {
let context = this;
const args = arguments;
if (timeout) clearTimeout(timeout)
if (immediate) { // 第一次触发时,timeout为null,立即执行一次回调
fn.apply(context, args);
immediate = false
} else {
timeout = setTimeout(function () {
fn.apply(context, args);
}, delay)
}
}
}
}
**新增需求:
1、事件触发,立刻执行函数。然后等到停止触发后n秒,才能重新触发事件
2、增加immediate 参数判断是否是立刻执行
function debounce(func, wait, immediate=flase) {
var timeout;
return function () {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
if (!timeout) func.apply(context, args) //**clearTimeout(timeout) 清掉的是计时器任务, 而timeout的值还在
timeout = setTimeout(function(){
timeout = null;
}, wait)
}
else {
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}
}
**新增需求:
1、事件触发,立刻执行函数,然后等到停止触发后 n 秒才执行函数。
2、事件再次触发,立刻执行函数,然后等到停止触发后 n 秒才执行函数。
3、增加immediate 参数判断是否是立刻执行
function debounce(func, delay, immediate) {
var timeout
return function() {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout)
if (immediate) {
if (!timeout) func.apply(context, args)
timeout = setTimeout(function () {
func.apply(context, args)
timeout = null
}, delay)
} else {
timeout = setTimeout(function () {
func.apply(context, args)
}, delay)
}
}
}
}
**最终版本
function debounce(func, wait, immediate) {
var timeout, result;
var debounced = function () {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
//当immediate=true 能返回函数的执行结果
//当immediate=false setTimeout的返回值是id值,不能返回函数的执行结果
if (!timeout) result = func.apply(context, args) 。
timeout = setTimeout(function () {
func.apply()
timeout = null
}, delay)
}else {
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
return result;
};
debounced.cancel = function() { //取消 debounce 函数
clearTimeout(timeout);
timeout = null;
};
return debounced;
}
函数防抖的应用场景
连续的事件,只需触发一次回调的场景有:
- 搜索框搜索输入。只需用户最后一次输入完,再发送请求
- 窗口大小Resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。
3.2 节流
节流是持续触发的时候,每 n 秒执行一次函数
有两种主流的实现方式,一种是使用时间戳,一种是设置定时器。
**时间戳
function throttle(func, wait) {
var context, args;
var previous = 0;
return function() {
var now = +new Date();
context = this;
args = arguments;
if (now - previous > wait) { 立刻执行第一次
func.apply(context, args);
previous = now;
}
}
}
**定时器
function throttle(func, wait) {
var timeout;
return function() {
context = this;
args = arguments;
if (!timeout) {
timeout = setTimeout(function(){ n 秒后第一次执行
timeout = null;
func.apply(context, args)
}, wait)
}
}
}
两者区别:
- 第一种事件会立刻执行,第二种事件会在 n 秒后第一次执行
- 第一种事件停止触发后没有办法再执行事件,第二种事件停止触发后依然会再执行一次事件
**新增需求:事件触发函数立刻执行,停止触发的时候还执行函数一次
function throttle(func, wait=1000) {
var timeout, context, args, result;
var previous = 0;
var throttled = function() {
context = this;
args = arguments;
var now = +new Date();
var remaining = wait - (now - previous);
// 第一次: now-previouse>1000 remaining<0 立刻执行函数
// 第二次: now-previouse<=1000 当remaining=1000&&!timeout 1000秒后执行函数, 如此反复
// 离去: 1、 now-previouse<1000 0<remaining<=1000 1000秒后执行函数,
// 2、 now-previouse>1000 remaining<0 立刻执行函数
if (remaining <= 0 || remaining > wait) { // 如果没有剩余的时间了或者你改了系统时间
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
func.apply(context, args);
previous = now;
} else if (!timeout) {
timeout = setTimeout(function() {
previous = +new Date();
func.apply(context, args)
timeout = null;
}, remaining);
}
};
return throttled;
}
**优化:有时也希望无头有尾,或者有头无尾, 或是有头有尾
1、设置个 options 作为第三个参数
2、leading:false 表示禁用第一次执行
3、trailing: false 表示禁用停止触发的回调
function throttle(func, wait, options) {
var timeout, context, args, result;
var previous = 0;
if (!options) options = {};
var throttled = function() {
context = this;
args = arguments;
var now = +new Date();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
func.apply(context, args);
previous = now;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(function() {
func.apply(context, args);
previous = +new Date();
timeout = null;
}, remaining);
}
};
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = null;
}
return throttled;
}
函数节流的应用场景
间隔一段时间执行一次回调的场景有:
- 滚动加载,加载更多或滚到底部监听
- 谷歌搜索框,搜索联想功能
- 高频点击提交,表单重复提交