前端面试JS突破-函数

54 阅读5分钟

如何模拟实现函数方法: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 引擎到此将直接执行函数。

特点

  1. 当函数变成立即执行的函数表达式时,表达式中的变量不能从外部访问。
(function () {
	var test = "Barry";
})();
// 无法从外部访问变量 test
console.log(test); // 抛出错误:"Uncaught ReferenceError: test is not defined"
  1. 将 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;
        }
    }
}