1.this的基本概念
- 1. this不能在执行期间被赋值(不能直接赋值整个对象this = a;但可以this.a = '1')
- 2. js中的this它不是固定不变的,是随着它执行环境的变化而改变
var bar = 1;
let obj = {
bar: 2,
foo: function(){
console.log(this.bar);
}
}
var fn = obj.foo;
obj.foo();//2 this指向obj
fn();//1 this执行全局对象window中的bar
2.this在面向对象中的使用
3.js严格模式下的this
严格模式下直接函数中直接调用this为undefined,非严格模式下为window
4.this指向问题
- 1. 在一般函数调用时,this指向的window
- 2. 在构造函数中,this指向实例化出来的对象
- 3. 作为对象方法调用,this指向调用对象
- 4. call和apply调用,this指向第一个调用的对象
4.1在一般函数调用时,this指向的window
function fn(){
console.log(this);//window
}
fn();
4.2在构造函数中,this指向实例化出来的对象
function Fn(){
this.a = 1;
console.log(this); //Fn {a: 1}
}
console.log(new Fn());//Fn {a: 1}
4.3 作为对象方法调用,this指向调用对象
var bar = 1;
let obj = {
bar: 2,
foo: function(){
console.log(this);//{bar: 2, foo: ƒ}
}
}
obj.foo();//2 this指向obj
4.4call和apply调用,this指向第一个调用的对象
var x = 0;
var obj1 = {
x:1
}
var obj2= {
x:2
}
function fn(){
console.log(this, this.x);
}
obj2.fn = fn;
obj2.fn(); //2 this执行obj2
obj2.fn.call(obj1); //1 this执行obj1
obj2.fn.call(); //0 不写参数,执行window
5.call、bind、apply的使用
5.1 call方法
语法: function.call(thisArg, arg1, arg2, ...)
。 其中thisArg是要设置为函数执行上下文的对象,也就是this要指向的对象,从第二个参数开始,arg1, arg2, ... 是传递给函数的参数。通过使用call方法,可以将一个对象的方法应用到另一个对象上。
注意 如果省略第一个 thisArg 参数,则默认为 undefined。在非严格模式下,this 值将被替换为 globalThis(类似于全局对象)
"use strict"
const person1 = {
fn: function () {
console.log(this)
}
}
const person2 = {
name: 'person2'
}
person1.fn.call()
5.2 apply
语法:function.apply(thisArg, [argsArray])
。 其中thisArg是要设置为函数执行上下文的对象,也就是this要指向的对象,argsArray是一个包含参数的数组。通过使用apply方法,可以将一个对象的方法应用到另一个对象上,并使用数组作为参数。
使用场景:ES5下将一个数组的元素合并到另一个数组,arr.push.apply(arr,arr2)
let arr = ['a','b'];
let arr2 = [1,2];
// 通过call方法可以将arr和arr2进行合并到arr中(ES6中可以使用扩展运算符)
// arr.push(...arr2);
// console.log(arr);//['a', 'b', 1, 2]
// ES5中可以使用apply
arr.push.apply(arr,arr2);
console.log(arr);//['a', 'b', 1, 2]
5.3 bind
语法:function.bind(thisArg, arg1, arg2, ...)
。 其中thisArg是要绑定到函数执行上下文的对象,也就是this要指向的对象,从第二个参数开始,arg1, arg2, ...是传递给函数的参数。与call和apply方法不同,bind方法并不会立即执行函数,而是返回一个新函数,可以稍后调用。这对于事件处理程序和setTimeout函数等场景非常有用。
const person1 = {
name: 'allen',
fn: function () {
console.log(this.name)
}
}
const fn = person1.fn
fn() // undefined
// 改写成,this指向person1
// const fn = person1.fn.bind(person1)
6.利用bind解决定时器参数的问题
// 利用bind给定时器传值
function fn(name){
console.log("Hello, "+name);
}
// delayFn(name); //直接传值是会报错的
const delayFn = fn.bind(null, 'lmf');
setTimeout(delayFn,1000);
8.call、bind、apply区别
-
call和apply都是直接调用函数,bind不会立即调用
-
call和bind的参数为参数列表,apply的参数需要以数组的形式传递
7.手写call、bind、apply
7.1手写call方法
原理:
-
首先,通过
Function.prototype.myCall
将自定义的myCall
方法添加到所有函数的原型对象上,使得所有函数实例都可以调用该方法。 -
在
myCall
方法内部,首先通过typeof this !== "function"
判断调用myCall
的对象是否为函数。如果不是函数,则抛出一个类型错误。 -
然后,判断是否传入了上下文对象
context
。如果没有传入,则将context
赋值为全局对象;ES11 引入了globalThis
,它是一个统一的全局对象,无论在浏览器还是Node.js
中,都可以使用globalThis
来访问全局对象。 -
接下来,使用
Symbol
创建一个唯一的键fn
,用于将调用myCall
的函数绑定到上下文对象的新属性上。 -
将调用
myCall
的函数赋值给上下文对象的fn
属性,实现了将函数绑定到上下文对象上的效果。 -
调用绑定在上下文对象上的函数,并传入
myCall
方法的其他参数args
(因为call方法是立即执行函数所以需要调用方法并执行)。 -
将绑定在上下文对象上的函数删除,以避免对上下文对象造成影响。
-
返回函数调用的结果。
为什么要将调用 myCall
的函数绑定到上下文对象的新属性上?
let obj1 = {
fn: function(){}
};
let obj2 = {
name:'lmf'
}
// 将obj1.fn的this执行改为obj2,且obj2的所有属性name也绑定到this上下文执行对象上
obj1.fn.call(obj2);
手写方法:
// 1.Function.prototype.myCall
Function.prototype.myCall = function (context, ...args) {
// 2.判断调用者是否为一个函数(如,obj1.fn.call这里的this即为obj1.fn函数)
if (typeof this !== 'function') {
throw new TypeError("Function.prototype.myCall - 被调用的对象必须是函数");
}
// 3.判断是否传入上下文对象(第一个参数)
// 如果没有传入上下文对象,则默认为全局对象
// ES11 引入了 globalThis,它是一个统一的全局对象
// 无论在浏览器还是 Node.js 中,都可以使用 globalThis 来访问全局对象。
context = context || globalThis;
// 4.通过Symbol创建唯一的fn防止命名冲突,用于调用函数的上下文对象绑定到新的属性上
let fn = Symbol('key');
// 5.将上下文对象绑定到新的属性上
context[fn] = this;
// 6.调用绑定好上下文的新函数,并传入参数,并接受
const result = context[fn](...args);
// 7.删除通过Symbol绑定的上下文新属性,以防对原有属性产生影响
delete context[fn];
// 8.返回调用的函数结果
return result;
}
// 测试手写的call方法
let obj1 = {
fn: function () {
console.log(this.name);
}
};
let obj2 = {
name: 'lmf'
}
// 将obj1.fn的this执行改为obj2,且obj2的所有属性name也绑定到this上下文执行对象上
obj1.fn.call(obj2); //lmf
obj1.fn.myCall(obj2); //lmf
7.2手写apply方法
原理:apply的实现思路跟call类似,就是apply传入参数是以数组的形式传入,所以多了一步判断传入的参数是否为数组以及在调用方法的时候使用扩展运算符 ...
将传入的参数数组 argsArr
展开
Function.prototype.myApply = function (context, argsArr) {
if (typeof this !== 'function') {
throw new TypeError("Function.prototype.myCall - 被调用的对象必须是函数");
}
if(argsArr && !Array.isArray(argsArr)){
throw new TypeError("Function.prototype.myApply - 第二个参数必须是数组")
}
context = context || globalThis;
let fn = Symbol('key');
context[fn] = this;
// 判断argsArr是否为一个数组(apply方法参数必须是一个数组),不是数组直接执行
const result = Array.isArray(argsArr) ? context[fn](...argsArr) : context[fn]();
delete context[fn];
return result;
}
// 测试手写的apply方法
let obj1 = {
fn: function () {
console.log(this.name);
}
};
let obj2 = {
name: 'lmf-apply'
}
// 将obj1.fn的this执行改为obj2,且obj2的所有属性name也绑定到this上下文执行对象上
obj1.fn.apply(obj2); //lmf-apply
obj1.fn.myApply(obj2); //lmf-apply
7.3手写bind方法
原理:
-
首先,通过
Function.prototype.myBind
将自定义的myBind
方法添加到所有函数的原型对象上,使得所有函数实例都可以调用该方法。 -
在
myBind
方法内部,首先通过typeof this !== "function"
判断调用myBind
的对象是否为函数。如果不是函数,则抛出一个类型错误。 -
然后,判断是否传入了上下文对象
context
。如果没有传入,则将context
赋值为全局对象;ES11 引入了globalThis
,它是一个统一的全局对象,无论在浏览器还是Node.js
中,都可以使用globalThis
来访问全局对象。 -
保存原始函数的引用,使用
_this
变量来表示。(因为调用bind后返回的函数可能会用new的方式再创建) -
返回一个新的闭包函数
fn
作为绑定函数(bind方法不是立即执行函数)。这个函数接受任意数量的参数innerArgs
。 -
在返回的函数
fn
中,首先判断是否通过new
关键字调用了函数。这里需要注意一点,如果返回出去的函数被当作构造函数使用,即使用new
关键字调用时,this
的值会指向新创建的实例对象。通过检查this instanceof fn
,可以判断返回出去的函数是否被作为构造函数调用。这里使用new _this(...args, ...innerArgs)
来创建新对象。 -
如果不是通过
new
调用的,就使用apply
方法将原始函数_this
绑定到指定的上下文对象context
上。这里使用apply
方法的目的是将参数数组args.concat(innerArgs)
作为参数传递给原始函数。 -
args表示myBind()创建时的23,12,使用...args是因为bind函数需要参数列表。而后面再次调用_This.apply(context, args.concat(innerArgs))时,apply参数是数组。arg就表示没有展开的数组
Function.prototype.myBind = function(context, ...args){
if(typeof this !== 'function'){
throw new TypeError("Function.prototype.myCall - 被调用的对象必须是函数");
}
context = context || globalThis;
// bind函数不会立即执行,所以不需要绑定新属性并执行
let _This = this;
// bind函数会返回一个函数,所以通过匿名函数方法返回。innerArgs表示调用bind后返回的函数执行时传入的参数
return function fn(...innerArgs){
// 判断如果fn通过new Fn()形式被调用( new delayFn(2,3))
if(this instanceof fn){ //判断fn在this的原型上
return new _This(...args,...innerArgs);
}
// 使用apply方法将原函数绑定到指定的上下文对象上
return _This.apply(context, args.concat(innerArgs));
}
}
// 测试手写的bind方法
let obj1 = {
hello: function () {
console.log(this.name);
}
};
let obj2 = {
name: 'lmf-bind'
}
// 将obj1.fn的this执行改为obj2,且obj2的所有属性name也绑定到this上下文执行对象上
let delayFn1 = obj1.hello.bind(obj2); //lmf-bind
let delayFn2 = obj1.hello.myBind(obj2,23,12); //lmf-bind
delayFn1(1,2);
delayFn2(1,2);
console.log(new delayFn1(3,4));
console.log(new delayFn2(3,4));
8.问题
手写call方法时第4步,为什么要将调用 myCall 的函数绑定到上下文对象的新属性上?
我的理解比如将obj1.fn的this改变为obj2,那么obj1的fn里面就可以通过this调用到obj2的name属性
但是obj1.fn.call(obj2);这句话真正的目的是想要将obj1.fn的this指向obj2,即通过obj2.fn也能调用obj1.fn的方法
当然最后是能实现将obj1.fn的this改变为obj2,那么obj1的fn里面就可以通过this调用到obj2的name属性
let obj1 = {
fn: function(){}
};
let obj2 = {
name:'lmf'
}
// 将obj1.fn的this执行改为obj2,且obj2的所有属性name也绑定到this上下文执行对象上
obj1.fn.call(obj2);(可以调用obj1上的fn)