第五章:函数—下-灵析社区

懒人学前端

四、如何模拟实现函数方法: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() 即可:

  1. 改造 foo 对象,给它添加 bar 方法
  2. 执行 bar 方法
  3. 复原 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"

2.将 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 未定义,它是立即调用函数表达式的私有变量

小练习

下面代码的输出结果是什么?如何让下面的代码输出结果为:0、1、2

for(var i = 0; i < 3; i++) {
  setTimeout(function(){
    console.log(i); 
  },1000)
}

答案:输出结果为: 3、3、3。可以如下改造以实现输出结果为: 0、1、2,实现如下:

for(var i = 0; i < 3; i++ ) {
	(function(j){
    setTimeout(function(){
      console.log(j);
    },1000);
  }(i))
}

六、箭头函数有什么特点?

箭头函数表达式的语法比函数表达式更简洁,并且没有自己的 thisargumentssupernew.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。

// 当只有一个参数时,圆括号是可选的:
(singleParam) => { statements }
singleParam => { statements }

// 没有参数的函数应该写成一对圆括号。
() => { statements }

引入箭头函数有两个方面的作用:更简短的函数并且不绑定 this

特点

没有单独的 this

箭头函数不会创建自己的 this,它只会从自己的作用域链的上一层继承 this

function Person() {
	this.age = 0;

	setInterval(() => {
		// this 正确地指向 p 实例
		console.log(this === p) // true
		this.age++; 
	}, 1000);
}

var p = new Person();

与严格模式的关系

鉴于 this 是词法层面上的,严格模式中与 this 相关的规则都将被忽略。

var f = () => { 'use strict'; return this; };
f() === window; // 或者 global

通过 call apply bind 调用

由于箭头函数没有自己的 this,通过这些方法调用一个函数时,只能传递参数,他们的第一个参数会被忽略,即不能绑定 this

let adder = {
	base: 1,
	add: function (a) {
		console.log(this === adder); // true
		let f = (v) => v + this.base;
		return f(a);
	},
	addThruCall: function (a) {
		let f = (v) => {
			console.log(this === adder); // true
			console.log('v 的值是 ' + v + ' ' + 'this.base 的值是 ' + this.base); // 'v 的值是 1 this.base 的值是 1'
			return v + this.base; 
		};
		let b = {
			base: 3
		};
		// call() 方法不能绑定 this 为 b 对象,第一个参数 b 被忽略了
		return f.call(b, a); 
	}
};

console.log(adder.add(1)); // 输出 2
console.log(adder.addThruCall(1)); // 输出 2

使用箭头函数作为方法

箭头函数中没有定义 this 绑定。

"use strict";
var obj = {
	i: 10,
	b: () => console.log(this.i, this), // undefined, Window{...}
	c: function () {
		console.log(this.i, this); // 10, Object {...}
	}
};
obj.b();
obj.c();

使用 new 操作符

箭头函数不能用作构造器,和 new 一起用会抛出错误。

var Foo = () => {};
var foo = new Foo(); // TypeError: Foo is not a constructor

作为匿名函数

ES6 的箭头函数表达式,可以作为匿名函数的简写:

// 匿名函数
let show = function () {
    console.log("匿名函数")
};
show() // "匿名函数"

let show1 = () => console.log("匿名函数");
show1(); // "匿名函数"

但是箭头函数和匿名函数在一些实际的操作中还有一些区别。

小练习

下面代码输出结果是什么?

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 返回一个箭头函数,箭头函数使用 call 绑定失效,第一个参数被忽略
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

七、如何实现防抖和节流?

在实际的业务开发中,会遇到一些频繁触发的事件,比如浏览器窗口的 resizesrcoll 等,处于性能的考虑,需要减少触发数量,或者延迟触发事件的时间等,因此就用到了防抖(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;
        }
    }
}

小练习

模拟实现防抖和节流。

答案:见正文。


阅读量:600

点赞量:0

收藏量:0