this & 作用域 & 闭包
this
的核心,在大多数情况下,可以简单地理解为谁调用了函数,this
就指向谁。但请注意,这里不包括通过call
、apply
、bind
、new
操作符或箭头函数进行调用的特殊情况。在这些特殊情况下,this
的指向会有所不同。
this
的值是在函数运行时根据调用方式和上下文来确定的。和作用域不同,作用域在代码写出来的那一刻就已经决定好了。
const o1 = {text: "o1",fn: function(){console.log("o1 this",this);return this.text;}
};const o2 = {text: "o2",fn: function(){console.log("o2 this",this);return o1.fn()}
};const o3 = {text: "o3",fn: function(){// 当通过 let fn = o1.fn; 将 o1.fn 赋值给局部变量 fn 时,已经丢失了与 o1 的任何关联。let fn = o1.fn;return fn();}
}console.log("o1fn",o1.fn());
// o1调用fn,所以先打印o1对象,再打印 "o1"
console.log("o2fn",o2.fn());
// o2调用fn,所以先打印o2对象,再打印 o1对象,最后打印 "o1"
console.log("o3fn",o3.fn());
// o3调用fn,此时fn没有调用对象,所以this指向默认的window对象,window没有text属性,所以this.text返回undefined
你看明白了吗?
那提问o2.fn
执行时怎么让最终的结果改成"o2"呢?
// 1. 可以直接借用函数,在o2本地去调用函数
const o1 = {text: "o1",fn: function(){console.log("o1 this",this);return this.text;}
};const o2 = {text: "o2",fn: o1.fn
};
console.log("o2fn",o2.fn())
// 2. 通过call apply bind去显示指定this
const o1 = {text: "o1",fn: function(){console.log("o1 this",this);return this.text;}
};const o2 = {text: "o2",fn: function(){console.log("o2 this",this);return o1.fn.call(o2) // o1.fn.call(this) o2调用fn时,this就是指向o2}
};
console.log("o2fn",o2.fn());
call & apply & bind
- 相同点:三者都是用来改变函数调用时的this指向
- 不同:
- call
functionName.call(thisArg, arg1, arg2, ...)
参数需要一个一个传递 - apply
functionName.apply(thisArg, [argsArray])
参数可以用数组传递 - bind
functionName.bind(thisArg[, arg1[, arg2[, ...]]])
- 返回一个新函数,在
bind()
被调用时,这个新函数的this
被指定为bind()
的第一个参数,而其余参数将作为新函数的参数供调用时使用。
- call
call
和apply
都会调用函数,并返回函数调用的结果(如果有的话)。bind
不会调用函数,而是返回一个新的函数,这个新函数在被调用时才会执行原始函数,并且具有指定的this
值和预置的参数。- 使用场景
- 如果知道要传递的参数,并且想要立即调用函数,那么可以使用
call
或apply
。 - 如果想要创建一个新函数,这个函数在被调用时具有特定的
this
值和预置的参数,那么可以使用bind
。
- 如果知道要传递的参数,并且想要立即调用函数,那么可以使用
有时我们会遇到要手写这三个函数的情况,我们可以先写一个大致的框架,列出函数的输入输出,然后再向框架里填充内容。
call
function hello(start, end) { return start + ', ' + this.name + end;
} const person = { name: 'Alice' }; // 使用 call 调用 hello 函数,并将 this 绑定到 person 对象
const message = hello.call(person, 'Hello', '!');
console.log(message); // 输出 "Hello, Alice!"
手写call
输入:一个上下文,可选参数(一个一个传递)
输出:函数执行的结果
/*
function speak() {console.log(this.name,'can speak!'); // Alice can speak
}const obj1 = {name: 'Alice'
}
speak.myCall(obj1);
*/
Function.prototype.myCall = function (context,...args) {// 边界检测,如果context没传则将上下文替换成全局对象window||globalcontext = context || window;// 将调用myCall的函数(这里指的是speak)作为新的上下文(调用myCall时传入的obj1)的属性值添加到上下文中// 注意:这里我们用context.fn作为中间变量来调用函数context.fn = this; // // 将this赋值给context的一个属性const result = context.fn(...args); // 使用context作为上下文调用函数delete context.fn; // 清理环境,避免内存泄漏return result;
}
在这个例子中,speak
是被调用的函数,所以 this
在 myCall
内部指向 speak
函数。而 context
是我们传递给 myCall
的 obj1
对象,我们希望在 speak
函数内部使用 obj1
作为 this
上下文。因此,我们将 speak
函数作为 obj1
的一个方法(临时)来调用它,实现了改变 this
上下文的效果。
中途打断点可以看到context
的值如图
apply
// Math.max() 函数不接受数组作为参数。它接受任意数量的数字参数,并返回这些参数中的最大值。所以这是最适合用于演示apply的函数
function max(numbers) { return Math.max.apply(null, numbers);
} const maxNum = max([1, 2, 3, 4, 5]);
console.log(maxNum); // 输出 5
手写apply
经过上面的例子手写了call
之后,手写apply
就没什么难的了,因为它俩就接受参数的方式不同而已。apply
接受的是一个数组
Function.prototype.myApply = function (context,argumentsArr) {context = context || window;context.fn = this;// 如果argumentsArr存在则将其内容作为参数传递const result = argumentsArr ? context.fn(...argumentsArr) : context.fn();delete context.fn;return result
}// 将上面的Math.max.apply改成myApply是一样的效果
bind
比如当我们想要确保某个函数总是以特定的上下文来执行时。例如,在事件处理器、回调函数和定时器中,我们需要绑定 this
上下文,确保函数总是能够正确地访问和操作我们期望的对象。
1. 事件处理器中的 this
绑定
在事件处理器中,this
通常指向触发事件的元素,而不是我们期望的对象。使用 bind
可以确保 this
指向正确的对象。
function Button() {this.name = 'My Button';this.handleClick = function(event) {console.log(this.name + ' was clicked!'); // 'this' 指向Button实例};const buttonElement = document.getElementById('btn');buttonElement.addEventListener('click', this.handleClick.bind(this));
}
const myButton = new Button();
// 输出:My Button was clicked!// 如果改成下面这个则不会输出name,只会输出 was clicked!
// buttonElement.addEventListener('click', this.handleClick);
2. 回调函数中的 this
绑定
在异步操作或回调函数中,this
的值可能会变化。使用 bind
可以确保 this
的值在回调函数执行时保持不变。
function User(firstName) {this.firstName = firstName;this.fetchData = function(url) {// 假设fetch是一个模拟的异步函数fetch(url).then(response => response.json()).then(data => {console.log(this.firstName + ' fetched data: ', data); // 'this' 指向User实例}.bind(this)) // 使用bind确保this指向User实例.catch(error => console.error('Error:', error));};
}const user = new User('Alice');
user.fetchData('https://api.example.com/data');
注意:在现代JavaScript中,通常使用箭头函数来自动绑定 this
,因为箭头函数不绑定自己的 this
,而是捕获其所在上下文的 this
值。
3. 预设参数
使用 bind
可以预设函数的参数。这在创建可复用的函数时非常有用。
function list() {return Array.prototype.slice.call(arguments);
}const list1 = list(1, 2, 3); // [1, 2, 3]// 创建一个新的函数,预设第一个参数为'boys'
const listWithItems = list.bind(null, 'boys');
const list2 = listWithItems(1, 2, 3); // ['boys', 1, 2, 3]
在这个例子中,listWithItems
是 list
函数的一个新版本,将 'boys'
作为第一个参数。当我们调用 listWithItems(1, 2, 3)
时,它实际上是在调用 list('items', 1, 2, 3)
。
4. 绑定到特定的上下文
有时我们可能希望将函数绑定到特定的对象,以便在其他地方调用它时,它总是以该对象为上下文。
const obj = {age: 10,getAge: function() {return this.age;}
};const unboundGetAge = obj.getAge;
console.log(unboundGetAge()); // undefined,因为this没有绑定到objconst boundGetAge = obj.getAge.bind(obj);
console.log(boundGetAge()); // 10,因为this被绑定到obj
在这个例子中,unboundGetAge
在调用时没有绑定 this
,所以它的 this
值是 window。而 boundGetAge
则被绑定到 obj
对象,因此它总是返回 obj.age
的值。
手写bind函数
// 手写bind
// 输入:一个新的this上下文,以及可选的参数列表
// 输出:一个新的函数,这个新函数被调用时会将this设置为指定的值,并且将参数列表与bind调用时提供的参数合并
Function.prototype.myBind = function (context,...initialArgs){// 1.保存调用 myBind 方法的原始函数。const self = this;// 2.返回一个新函数return function F(...boundArgs) {// 3.判断函数是否以构造函数的方式调用 这一段我还没理解if (this instanceof F) {// 如果是,那么 this 会指向一个新创建的对象,而不是我们提供的 context。// 我们就使用new和原始函数self来调用return new self(...initialArgs,...boundArgs);}// 否则,直接调用原始函数self,并传入context作为this,以及合并后的参数return self.apply(context,[...initialArgs,...boundArgs]);}
}/*
function hello(start, end) { return start + ', ' + this.name + end;
} const obj = { name: 'Alice' }; const boundHello = hello.myBind(obj, 'Hello'); console.log(boundHello('!')); // 输出 "Hello, Alice!"
*/
上面这段代码执行栈如下图,context
就是我们传入的obj对象,initialArgs
是我们在调用bind是传入的'Hello'
,boundArgs
是我们调用返回的新函数boundHello
时传入的参数。
当我们使用 bind
方法(无论是原生的 Function.prototype.bind
还是手写实现的 myBind
)时,我们实际上是在创建一个新的函数,这个函数被“绑定”到了特定的 this
上下文(在这个例子中是 obj
对象)以及一些预先设定的参数(在这个例子中是 'Hello'
)。
这个新函数(我们称之为 boundHello
)现在可以独立使用,并且每次调用它时,都会以我们指定的 this
上下文(obj
)和预先设定的参数('Hello'
)来调用原始的 hello
函数。