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()
的区别有两点:
模拟实现如下:
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
(立即调用函数表达式)是一个在定义时就会立即执行的 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))
}
箭头函数表达式的语法比函数表达式更简洁,并且没有自己的 this
,arguments
,super
或 new.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
在实际的业务开发中,会遇到一些频繁触发的事件,比如浏览器窗口的 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;
}
}
}
小练习
模拟实现防抖和节流。
答案:见正文。
阅读量:600
点赞量:0
收藏量:0