如何模拟实现函数方法:call()、apply()、bind()?
面试高频指数:★★★★★
call()、apply()、bind() 三个方法都可以根据指定的 this 值调用。
call()
MDN 上 call() 的定义:
call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。 let foo = { value: 1 };
function bar() {
console.log(this.value); // 1
}
bar.call(foo);
由上面代码可知:
- call 改变了 this 的指向,指向了 foo
- bar 函数执行了
因此,我们只要按照下面的步骤来实现 call() 即可:
- 改造 foo 对象,给它添加 bar 方法
- 执行 bar 方法
- 复原 foo 对象,把添加的 bar 方法删掉
下面是对应的代码实现:
Function.prototype.myCall = function(thisArg, ...args) {
// 判断参数指定的 this 的类型
// call() 函数在 thisArg 参数为 undefined 或者为 null 时,会将 thisArg 自动指向全局对象
if(thisArg === undefined || thisArg === null) {
thisArg = typeof window === 'undefined' ? global : window;
}
// 为了避免覆盖 thisArg 上面同名的方法/属性,我们借用 Symbol 生成对应属性名
const key = Symbol('fn');
// 改造对象,添加方法
thisArg\[key] = this;
// 执行方法
const result = thisArg[key](...args);
// 复原对象,删除方法
delete thisArg\[key];
return result;
}
apply()
该方法的语法和作用与 call() 方法类似,只有一个区别,就是 call() 方法接受的是一个参数列表,而 apply() 方法接受的是一个包含多个参数的数组。 apply() 的模拟实现:
Function.prototype.myApply = function (thisArg, args) {
if (thisArg === null || thisArg === undefined) {
thisArg = typeof window === undefined ? global : window
}
thisArg = Object(thisArg)
const key = Symbol('fn')
thisArg[key] = this
const result = args ? thisArg[key](...args) : thisArg[key]()
delete thisArg[key]
return result
}
bind()
MDN 对 bind() 的定义:
bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
bind()和call()的区别有两点:
- 它返回的是一个新的函数
- 如果使用
new运算符调用,则忽略传入的this值
模拟实现如下:
Function.prototype.myBind = function (context) {
// 保存 this 指向
const self = this;
// 获取参数
const args = Array.prototype.slice.call(arguments, 1);
// 构造原型链
const F = function () {};
F.prototype = this.prototype;
// 创建新的函数
const bound = function () {
// 获取其余参数
const innerArgs = Array.prototype.slice.call(arguments);
// 将两次获取的参数拼接起来
const finnalArgs = args.concat(innerArgs);
// 判断是否是作为构造函数调用
return self.apply(this instanceof F ? this : context, finnalArgs);
};
bound.prototype = new F();
return bound;
};
立即调用函数表达式(IIFE)有什么特点?
面试高频指数:★★★★★
IIFE(立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。
(function () { // statements })();
主要包含两部分:
- 包围在圆括号运算符 () 中的一个匿名函数,这个匿名函数拥有独立的词法作用域。这不仅避免了外界访问此 IIFE 的变量,而且又不会污染全局作用域。
- 再一次使用 () 创建了一个立即执行函数表达式,JavaScript 引擎到此将直接执行函数。
特点
- 当函数变成立即执行的函数表达式时,表达式中的变量不能从外部访问。
(function () {
var test = "Barry";
})();
// 无法从外部访问变量 test
console.log(test); // 抛出错误:"Uncaught ReferenceError: test is not defined"
- 将 IIFE 分配给一个变量,不是存储 IIFE 本身,而是存储 IIFE 执行后返回的结果。
var result = (function () {
var name = "Barry";
return name;
})();
// IIFE 执行后返回的结果:
result; // "Barry"
运用:模块化封装
let counter = (function () {
let i = 0;
return {
get: function () {
return i;
},
set: function (val) {
i = val;
},
increment: function () {
return ++i;
}
};
})();
counter.get(); // 0
counter.set(3);
counter.increment(); // 4
counter.increment(); // 5
conuter.i; // undefined (`i`不是返回对象的属性)
i; // ReferenceError: i 未定义,它是立即调用函数表达式的私有变量
箭头函数
var name = "window";
var person1 = {
name: "person1",
foo1: function () {
console.log(this.name);
},
foo2: () => console.log(this.name),
foo3: function () {
return function () {
console.log(this.name);
};
},
foo4: function () {
return () => {
console.log(this.name);
};
}
};
var person2 = { name: "person2" };
person1.foo1(); // person1
person1.foo1.call(person2); // person2
person1.foo2(); // window
// foo2 返回一个箭头函数,箭头函数没有 this
person1.foo2.call(person2); // window
person1.foo3()(); // window
person1.foo3.call(person2)(); // window
person1.foo3().call(person2); // person2
person1.foo4()(); // person1
person1.foo4.call(person2)(); // person2
// 箭头函数的 this 在定义的时候就被指定了,之后改变不了
person1.foo4().call(person2); // person1
如何实现防抖和节流?
在实际的业务开发中,会遇到一些频繁触发的事件,比如浏览器窗口的 resize、srcoll 等,处于性能的考虑,需要减少触发数量,或者延迟触发事件的时间等,因此就用到了防抖(debounce)和节流(throttle)。
防抖
防抖的原理就是:用户可以尽管触发事件,但是一定在事件触发 n 秒后才执行。如果在此期间又触发了这个事件,那么就从新触发的时间点算起,n 秒后才执行。
防抖是根据延迟的时间点触发事件的。我们可以如下实现:
function debounce(cb, delay = 250) {
let timeout;
return (...args) => {
// 清除未到延迟时间的定时器
clearTimeout(timeout);
timeout = setTimeout(() => {
// 到延迟时间执行回调函数
cb(...args);
}, delay);
};
}
节流
节流是以时间段为节点,如果事件触发在这个时间段内,那么就只触发一次。
有两种实现方法:
- 使用定时器
- 使用时间戳
使用定时器
使用标识变量 shouldWait, 如果事件执行了则不再执行任何操作,否则,执行事件,并且修改标识变量 shouldWait。
function throttle(cb, delay = 250) {
let shouldWait = false;
return (...args) => {
// 如果应该等待,不执行
if (shouldWait) return;
// 否则,执行回调函数
cb(...args);
// 修改标识变量
shouldWait = true;
setTimeout(() => {
// 到延迟时间之后,重新修改标识变量,确保可以开启新的节流
shouldWait = false;
}, delay);
};
}
使用时间戳
计算时间范围,如果在此时间范围内,则不执行事件,否则执行事件并更新时间 previous。
function throttle(cb, delay) {
// 设置初始时间
let previous = 0;
return (...args) => {
// 设置当前时间
let now = +new Date();
// 根据时间差判断是否要执行事件
if (now - previous > delay) {
// 如果已经超过延迟时间,执行事件
cb(args);
// 重置初始时间
previous = now;
}
}
}