首先来看一道网上很经典的面试题。
问:以下代码会输出什么?
for(var i = 0;i < 5; i ++) {
setTimeout(() => {
console.log(i)
}, i * 1000)
}
答案是 每隔一秒输出一个5。
那么就会有接下来的问题,怎么修改才能输出0 1 2 3 4这样的结果呢?
es6中的let
for(let i = 0;i < 5; i ++) {
setTimeout(() => {
console.log(i)
}, i * 1000)
}
let相比于var会多一个块级作用域的概念。
闭包的形式实现
for(var i = 0;i < 5; i ++) {
(function(j) {
setTimeout(() => {
console.log(j)
}, j * 1000)
})(i)
}
setTimeout的第三个参数
for(var i = 0;i < 5; i ++) {
setTimeout((j) => {
console.log(j)
}, i * 1000, i)
}
以上是一些常见的答案,当然,当我答出这些之后,很明显没有满足面试官的需求。于是,面试官问了句,还有别的方法吗,词穷之下我没有给出其他答案,他提示了我一下考虑使用bind。然而,我不会用啊T_T…
为此,我特地去查阅了一下bind的用法,发现很多文章是把bind,call,apply放在一起讲解。因为他们的作用很像,只是中间过程有点区别。
bind
首先看下bind在MDN上的定义。bind()方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
由此我们可以知道,通过bind绑定过后的结果是一个新的函数,类似于return function的感觉。同时我们可以知道,bind() 方法的主要作用就是改变函数的this指向(万恶的this,不得不说JavaScript的this确实有点混乱)。光说可能会有点难以理解,还是通过几个实例来看一下bind()方法吧。
一个🌰
function fn(a, b, c) {
return a + b + c
}
var _fn = fn.bind(null, 10)
var res = _fn(20, 30)
console.log(res) // 60
此时打印出来的数值是60。fn函数本来需要三个参数,在调用bind之后将第一个参数也就是10传入到fn中,后面在调用_fn的时候传入后面的几个参数,这个时候如果调用 _fn(20,30,40) 的话,也不用担心,参数只会取到前两个。所以,bind除了第一个参数之外,剩下的都会顺序的传入到被绑定的函数体内。
new
在官方定义中特别的提到了new操作符,并说明了new的优先级要高于bind。 使用bind()返回的是一个function,那么就可以被new调用,根据规范可知,当使用new操作符调用的时候,bind的第一个参数无效。
另一个🌰
function Person(name, age) {
this.name = name;
this.age = age;
}
var _Person = Person.bind({});
var p = new _Person('hanzichi', 30); // Person {name: "hanzichi", age: 30}
上面例子中this指向并没有被改变。
同样的,我在查阅的过程中还发现了许多提到丢失this的可能,比如异步回调的过程中就很容易发生。
setTimeout&setInterval
var canvas = {
render: function() {
this.draw();
},
draw: function() {
...
}
}
setInterval(canvas.render, 2000)
上面代码中就会出现丢失this的问题,即this的指向会变成window。产生这个问题的原因就是由于setTimeout和setInterval的语法是 (fn, timeout)。第一个参数fn是一个传递过程,也就是说上面代码会变成下面这个样子
setInerval(fn = canvas.render, 2000)
所以this的指向就变成了window。这个时候我们就可以通过bind函数来强行改变this指向。
setInterval(canvas.render.bind(canvas), 2000)
call和apply
这两个方法放在一起说是因为在mdn的文档上就很明白的标注这,这两个语法和作用都类似,只有一个区别,就是call()接收的是参数列表,而apply()接受的是一个数组。
call和apply的作用
- 改变this指向
var obj = {
name: 'ssss'
}
function fn() {
console.log(this.name)
}
fn.call(obj)
call方法的第一个参数是函数上下文的对象,也就是改变this的关键所在。上面代码中将obj通过call传入了fn中,因此代码演变成了下面这样
function fn() {
console.log(obj.name) // ssss
}
- 借用别的对象的方法
function Person1() {
this.name = 'person1'
}
function Person2() {
this.getName = function() {
console.log(this.name)
}
Person1.call(this)
}
var person = new Person2()
person.getName() // person1
或者是通过js获取到的domlist本身是一个类数组,是无法调用数组方法的,此时我们也可以通过call或者apply来实现。
var nodes = Array.prototype.slice.call(document.getElementsByTagName('*'))
判断变量是否是数组
function isArray(obj) {
return Object.prototype.toString.call(obj) === '[object Array]'
}
接下来我们再通过一个经典的面试题来巩固我们刚才的知识。
定义一个log函数,用来代理console.log方法。
function log(msg) {
console.log(msg)
}
log(1) // 1
log(1, 2) // 1
本质实现并不复杂,但是当传入多个或者不确定个参数的时候就会出现问题。因此我们需要对方法进行改造优化一下。
function log() {
console.log.apply(console, arguments)
}
log(1) // 1
log(1, 2) // 1 2
现在需求又发生了变化,我们想要在每次输出之前加上一个 myApp 的前缀。
log('ssss') // myApp ssss
这个时候应该怎么优化呢?
function log() {
Array.prototype.unshift.call(arguments, 'myApp ')
console.log.apply(console, arguments)
}
通过call让类数组得以调用数组原型上的unshift方法,最后将结果打印出来。
bind和call、apply的区别
call和apply会立刻执行,而bind方法返回一个函数,需要再次调用函数才会执行。
最后,各个大厂都会要求的造火箭环节。手写bind,call,apply三个函数。
手写call
首先再来看下call的作用,然后我们根据他实现的效果来实现需求。
var obj = {
name: 'sss',
getName: function() {
console.log(this)
console.log(this.name)
},
}
obj.getName() // sss
var obj1 = {
name: 'www'
}
obj.getName.call(obj1) // www
通过上面的知识我们知道call的过程是改变了this的指向。所以有以下实现
Function.prototype.myCall = function(context) {
// this是函数内部的this指向
// context是传入的新的调用者 在context上扩展一个新的属性指向this
context.getName = this
context.getName()
}
好的,初步的实现已经完成了,但是考虑到各种情况的出现,我们需要对这个方法进行优化兼容。
参考call的做法
- 当没有context的时候,我们需要给出一个默认值,也就是window。
- 接下来,context.getName这个属性是很容易冲突的,那么如何可以保证新申请的属性绝对不冲突呢?这里我们就可以联想到es6新出的symbol属性。
- 还有就是call是支持更多参数的传入的,所以我们可以考虑将参数带入到新的方法当中。
所以,优化过后的代码就会是这样。
Function.prototype.myCall = function(context, ...args) {
context = context || window
let symbol = Symbol()
context[symbol] = this
let fn = context[symbol](...args)
delete context[symbol]
return fn
}
此时一个call函数就实现完全啦。 apply方法和call方法只是对于参数不同而已,可以自己实现一下。
bind实现 bind方法与call和apply差别还是比较大的。因此实现起来也会有些不同。
Function.prototype.myBind = function(context) {
context = context || window
const _this = this
funtion fn(args) {
_this.apply(this, args)
}
return fn
}
好的,关于这三个方法的理解到这里就说完啦,欢迎大家来评论指正~