JS中this的应用场景,再了解下apply、call和bind!

简介: 在下面的这边文章中,将讲解关于this的几大应用场景以及了解在面试中经常会被问到的apply、bind和call究竟是什么,接下来开始进入本文的讲解。

一、谈谈对this对象的理解


  • this ,函数执行的上下文,总是指向函数的直接调用者(而非间接调用者),可以通过 applycallbind 改变 this 的指向。
  • 如果有 new 关键字,this 指向 new 出来的那个对象。
  • 在事件中,this 指向触发这个事件的对象,特殊的是,IE 中的 attachEvent 中的 this 总是指向全局对象 window
  • 对于匿名函数或者直接调用的函数来说,this 指向全局上下文(浏览器为 windowNodeJSglobal),剩下的函数调用,那就是谁调用它, this 就指向谁。
  • 对于 es6 的箭头函数,箭头函数的指向取决于该箭头函数声明的位置,在哪里声明, this 就指向哪里。


二、this的应用场景


在程序中,this主要有以下5种应用场景:

  • 作为普通函数被调用
  • 使用 callapplybind 被调用
  • 作为对象方法被调用
  • class 方法中被调用
  • 箭头函数中被调用


1、作为普通函数被调用


this 作为普通函数被调用时,指向 window 全局。

function fn1(){
    console.log(this);
}
fn1(); //window


2、使用call、apply和bind被调用


this 使用 callapplybind 被调用时,直接指向作用域内的内容。

function fn1(){
    console.log(this);
}
fn1(); //window
fn1.call({ x : 100 }); //{x : 100}
fn1.apply({x : 200}); //{x : 200}
const fn2 = fn1.bind({ x : 200 });
fn2(); //{ x : 200 }


3、作为对象方法被调用


从下面代码中可以得出,当 this 放在 sayHi() 方法里面时,此时作为 zhangsan 对象的方法被调用,指向的是当前的对象。而放在 wait() 方法时,里面还有一个定时器,定时器里面还有一个函数,所以第二个 this 是作为普通函数被调用,指向 window 全局。

const zhangsan = {
    name: '张三',
    sayHi(){
        //this 即当前对象
        console.log(this);
    },
    wait(){
        setTimeout(function(){
            //this === window
            console.log(this);
        });
    }
}


4、在class方法中被调用


从以下代码中可以看出,当 thisclass 中被调用时,指向的是整个对象。

class People{
    constructor(name){
        this.name = name;
        this.age = 20;
    }
    sayHi(){
        console.log(this);
    }
}
const zhangsan = new People('张三');
zhangsan.sayHi(); //zhangsan 对象


5、箭头函数中被调用


看到以下代码,细心的小伙伴不难发现,跟我们上面第3点看到的似乎有点类似,主要区别在于定时器中的函数改为了箭头函数。当改为箭头函数时,此时的this指向的是zhangsan这一个整个对象,而不再是指向全局。

const zhangsan = {
    name: '张三',
    sayHi(){
        //this 即当前对象
        console.log(this);
    },
    waitAgain(){
        setTimeout(() => {
            //this 即当前对象
            console.log(this);
        });
    }
}

讲完箭头函数,我们来梳理下箭头函数和普通函数的区别,以及箭头函数是否能当做是构造函数的问题。


(1)箭头函数和普通函数定义


普通函数通过 function 关键字定义, this 无法结合词法作用域使用,在运行时绑定,只取决于函数的调用方式,在哪里被调用,调用位置。(取决于调用者,和是否独立运行)

箭头函数使用被称为 “胖箭头” 的操作 => 定义,箭头函数不应用普通函数 this 绑定的四种规则,而是根据外层(函数或全局)的作用域来决定 this ,且箭头函数的绑定无法被修改( new 也不行)。


(2)箭头函数和普通函数的区别


  • 箭头函数常用于回调函数中,包括事件处理器或定时器。
  • 箭头函数和 var self = this ,都试图取代传统的 this 运行机制,将 this 的绑定拉回到词法作用域。
  • 箭头函数没有原型、没有 this 、没有 super,没有 arguments ,没有 new.target
  • 箭头函数不能通过 new 关键字调用。
  • 一个函数内部有两个方法:[[Call]][[Construct]],在通过 new 进行函数调用时,会执行 [[construct]]  方法,创建一个实例对象,然后再执行这个函数体,将函数的 this 绑定在这个实例对象上。
  • 当直接调用时,执行 [[Call]] 方法,直接执行函数体。
  • 箭头函数没有 [[Construct]] 方法,不能被用作构造函数调用,当使用 new 进行函数调用时会报错。
function foo(){
    return (a) => {
        console.log(this.a);
    }
}
let obj1 = {
    a: 2
};
let obj2 = {
    a: 3
};
let bar1 = foo.call(obj1);
let bar2 = bar1.call(obj2);
console.log(bar1); // 2 [Function (anonmous)]
// console.log(bar2); // 2 undefind


(3)this绑定的四大规则


this绑定四大规则遵循以下顺序:

New 绑定 > 显示绑定 > 隐式绑定 > 默认绑定

下面一一介绍四大规则。

  • 默认绑定:没有其他修饰( bindapplycall ),在非严格模式下定义指向全局对象,在严格模式下定义指向 undefined
function foo() { 
    console.log(this.a); 
}
var a = 2; 
foo(); //undefined
  • 隐式绑定:调用位置是否有上下文对象,或者是否被某个对象拥有或者包含,那么隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。而且,对象属性链只有上一层或者最后一层在调用位置中起作用。
function foo() { 
    console.log(this.a); 
}
var obj = { 
    a: 2, 
    foo: foo, 
}
obj.foo(); // 2
  • 显式绑定:通过在函数上运行 callapply ,来显式的绑定 this
function foo() { 
    console.log(this.a); 
}
var obj = { 
    a: 2 
};
foo.call(obj); //2

显示绑定之硬绑定

function foo(something) { 
    console.log(this.a, something); 
    return this.a + something; 
}
function bind(fn, obj) { 
    return function() {
        return fn.apply(obj, arguments); 
    }; 
}
var obj = { 
    a: 2 
}
var bar = bind(foo, obj);
console.log(bar); //f()
  • New 绑定new 调用函数会创建一个全新的对象,并将这个对象绑定到函数调用的 thisNew 绑定时,如果是 new 一个硬绑定函数,那么会用 new 新建的对象替换这个硬绑定 this
function foo(a) { 
    this.a = a; 
}
var bar = new foo(2); 
console.log(bar.a); //2


三、apply、call和bind



1、apply、call和bind的共同用法

先说下三者的共同用法,三者的共同用法就是可以改变函数的this指向,并将函数绑定到上下文中。接下来讲述一个应用场景加深理解:

let obj1 = {
    hobby: 'running',
    add(favorite){
        console.log(`在我的业余时间里,我喜欢${favorite},但同时我也喜欢${this.hobby}`);
    }
}
let obj2 = {
    hobby: 'learning'
}
obj1.add('reading'); //在我的业余时间里,我喜欢reading,但同时我也喜欢running

可以看到在最后一行代码中,我们调用了 obj1 中的 add 函数,并传入了一个参数 readingadd 函数中的 this 指的是他所在的对象 obj1 ,所以 this.hobby 就是 running , 但是我们如果想获得 obj2 中的hobby, 又该怎么处理呢?这就涉及到我们平常所听到的 applycallbind

接下来开始讲解 applycallbind


2、apply


(1)语法: Array.prototype.apply(this, [args1, args2])

(2)传入参数:

第一个参数:传入 this 需要指向的对象,即函数中的 this 指向谁,就传谁进来;

第二个参数:传入一个数组,数组中包含了函数需要的实参。

(3)apply的作用:①调用函数;指定函数中 this 的指向。

(4)代码演示:

/**
 * 
 * @description 实现apply函数,在函数原型上封装myApply函数, 实现和原生apply函数一样的效果
 */
Function.prototype.myApply = function(context){
    // 存储要转移的目标对象
    _this = context ? Object(context) : window;
    // 在转移this的对象上设定一个独一无二的属性,并将函数赋值给它
    let key = Symbol('key');
    _this[key] = this;
    // 将数组里存储的参数拆分开,作为参数调用函数
    let res = arguments[1] ? _this[key](...arguments[1]) : _this[key]();
    // 删除
    delete _this[key];
    // 返回函数返回值
    return res;
}

(5)前情回顾

实现了 myApply 之后,我们继续引用刚开始关于爱好的那个例子,来修改 this 的指向。

let obj1 = {
    hobby: 'running',
    add(...favorite){ //...favorite意味着可以接收多个参数
        console.log(`在我的业余时间里,我喜欢${favorite},但同时我也喜欢${this.hobby}`);
    }
}
let obj2 = {
    hobby: 'learning'
}
obj1.add.myApply(obj2, ['reading', 'working']); 
// 输出结果:在我的业余时间里,我喜欢reading,working,但同时我也喜欢learning

obj1.add.myApply(obj2, ['reading', 'working']) 这一行代码, 第一个参数将 obj1 中的 add 函数的 this 指向了 obj2 , 第二个参数以数组形式传入多个参数,作为 obj1 中的 add 函数传入的参数, 所以最后能将 readingworking 都输出。


3、call


(1)语法:Array.prototype.call(this, args1, args2)

(2)传入参数:

第一个参数:传入 this 需要指向的对象,即函数中的 this 指向谁,就传谁进来;

其余参数: 除了第一个参数,其他的参数需要传入几个,就一个一个传递进来即可。

(3)call的作用:①调用函数;指定函数中 this 的指向。

(4)代码演示:

/**
 * 
 * @description 实现apply函数,在函数原型上封装myApply函数, 实现和原生apply函数一样的效果
 */
Function.prototype.myCall = function(context){
    // 存储要转移的目标对象
    let _this = context ? Object(context) : window;
    // 在转移this的对象上设定一个独一无二的属性,并将函数赋值给它
    let  key = Symbol('key');
    _this[key] = this;
    // 创建空数组,存储多个传入参数
    let args = [];
    // 将所有传入的参数添加到新数组中
    for(let i =1; i < arguments.length; i++){
        args.push(arguments[i]);
    }
    // 将新数组拆开作为多个参数传入,并调用函数
    let res = _this[key](...args);
    // 删除
    delete _this[key];
    // 返回函数返回值
    return res;
}

(5)前情回顾

实现了 myCall 之后,我们继续引用刚开始关于爱好的那个例子,来修改 this 的指向。

let obj1 = {
    hobby: 'running',
    add(...favorite){ //...favorite意味着可以接收多个参数
        console.log(`在我的业余时间里,我喜欢${favorite},但同时我也喜欢${this.hobby}`);
    }
}
let obj2 = {
    hobby: 'learning'
}
obj1.add.myCall(obj2, 'reading', 'working');
// 输出结果:在我的业余时间里,我喜欢reading,working,但同时我也喜欢learning

obj1.add.myCall(obj2, 'reading', 'working') 这一行代码, 第一个参数将 obj1 中的 add 函数的 this 指向了 obj2 , 第二个参数通过依次传入多个参数的形式,作为 obj1 中的 add 函数传入的参数, 所以最后能将 readingworking 都输出。

讲到这里,我们来梳理下 callapply 的区别:

callapply 唯一的区别就是在于给函数传入参数的形式不同call 是将多个参数逐个传入, 而apply 是 将多个参数放在一个数组中,一起传入。


4、bind


(1)语法: Array.prototype.bind(this, args1, args2)

(2)传入参数:

第一个参数:传入 this 需要指向的对象,即函数中的 this 指向谁,就传谁进来;

其余参数: 除了第一个参数,其他参数的传递可以像 apply 一样的数组类型,也可以像 call 一样的逐个传入;但需注意的是后面需要加个小括号进行其余参数的传递。

(3)call的作用:①克隆当前函数,返回克隆出来的新函数;新克隆出来的函数,该函数的this被指定了。

(4)代码演示:

/**
 * @description 实现Bind函数,在函数原型上封装myBind函数 , 实现和原生bind函数一样的效果
 * 
 */
Function.prototype.myBind = function(context){
    // 存储要转移的目标对象
    let _this = context ? Object(context) : window;
    // 在转移this的对象上设定一个独一无二的属性,并将函数赋值给它
    let key = Symbol('key');
    _this[key] = this;
    // 创建函数闭包
    return function(){
        // 将所有参数先拆分开,再添加到新数组中,以此来支持多参数传入以及数组参数传入的需求
        let args = [].concat(...arguments);
        // 调用函数
        let res = _this[key](...args);
        // 删除
        delete _this[key];
        // 返回函数返回值
        return res;
    }
}

(5)前情回顾

实现了 myBind 之后,我们继续引用刚开始关于爱好的那个例子,来修改 this 的指向。

let obj1 = {
    hobby: 'running',
    add(...favorite){ //...favorite意味着可以接收多个参数
        console.log(`在我的业余时间里,我喜欢${favorite},但同时我也喜欢${this.hobby}`);
    }
}
let obj2 = {
    hobby: 'learning'
}
obj1.add.myBind(obj2)(['reading', 'working']);
// 输出结果:在我的业余时间里,我喜欢reading,working,但同时我也喜欢learning

通过以上我们可以看到, bind 有点类似 applycall 的结合,只不过它返回的是一个函数,需要自身再进行一次调用, 而传给这个函数的参数形式有两种方式,可以是像 apply 一样的数组形式, 也可以是像 call 一样的逐个传入的形式。

大家不要觉得这个后面加个小括号太麻烦,这就是 bind 的强大之处,有时候 bind 也会经常运用在函数柯里化中。

讲到这里,关于this的相关知识就讲完啦!接下来我们来做个总结。


5、做个小结


  • this 取什么样的值,是在函数执行时确定的,不是在函数定义的时候确定的。
  • applycallbind 三者都是函数的方法,都可以改变函数的 this 指向。
  • applycall 都是改变函数 this 指向,并传入参数后立即调用执行该函数。
  • bind 是在改变函数 this 指向后,并传入参数后返回一个新的函数,不会立即调用执行。
  • apply 传入的参数是数组形式的,call 传入的参数是按顺序的逐个传入并以逗号隔开, bind 传入的参数既可以是数组形式,也可以是按顺序逐个传入。


四、结束语



关于 this 的指向问题在前端的面试中尤为常见,大家可以按照上文中的顺序把 this 的知识点串联起来一起理解!同时,本文内容为本人理解所整理,可能会存在边界歧义等问题。如果有不理解或者有误的地方欢迎评论区评论或私信我交流~



相关文章
|
3月前
|
监控 JavaScript 算法
如何使用内存监控工具来定位和解决Node.js应用中的性能问题?
总之,利用内存监控工具结合代码分析和业务理解,能够逐步定位和解决 Node.js 应用中的性能问题,提高应用的运行效率和稳定性。需要耐心和细致地进行排查和优化,不断提升应用的性能表现。
222 77
|
30天前
|
缓存 NoSQL JavaScript
Vue.js应用结合Redis数据库:实践与优化
将Vue.js应用与Redis结合,可以实现高效的数据管理和快速响应的用户体验。通过合理的实践步骤和优化策略,可以充分发挥两者的优势,提高应用的性能和可靠性。希望本文能为您在实际开发中提供有价值的参考。
56 11
|
1月前
|
敏捷开发 人工智能 JavaScript
Figma-Low-Code:快速将Figma设计转换为Vue.js应用,支持低代码渲染、数据绑定
Figma-Low-Code 是一个开源项目,能够直接将 Figma 设计转换为 Vue.js 应用程序,减少设计师与开发者之间的交接时间,支持低代码渲染和数据绑定。
107 3
Figma-Low-Code:快速将Figma设计转换为Vue.js应用,支持低代码渲染、数据绑定
|
1月前
|
JavaScript 前端开发
【Vue.js】监听器功能(EventListener)的实际应用【合集】
而此次问题的核心就在于,Vue实例化的时机过早,在其所依赖的DOM结构尚未完整构建完成时就已启动挂载流程,从而导致无法找到对应的DOM元素,最终致使计算器功能出现异常,输出框错误地显示“{{current}}”,并且按钮的交互功能也完全丧失响应。为了让代码结构更为清晰,便于后续的维护与管理工作,我打算把HTML文件中标签内的JavaScript代码迁移到外部的JS文件里,随后在HTML文件中对其进行引用。
52 8
|
28天前
|
监控 安全 中间件
Next.js 实战 (十):中间件的魅力,打造更快更安全的应用
这篇文章介绍了什么是Next.js中的中间件以及其应用场景。中间件可以用于处理每个传入请求,比如实现日志记录、身份验证、重定向、CORS配置等功能。文章还提供了一个身份验证中间件的示例代码,以及如何使用限流中间件来限制同一IP地址的请求次数。中间件相当于一个构建模块,能够简化HTTP请求的预处理和后处理,提高代码的可维护性,有助于创建快速、安全和用户友好的Web体验。
|
3月前
|
存储 缓存 监控
如何使用内存监控工具来优化 Node.js 应用的性能
需要注意的是,不同的内存监控工具可能具有不同的功能和特点,在使用时需要根据具体工具的要求和操作指南进行正确使用和分析。
91 31
|
2月前
纸屑飘落生日蛋糕场景js+css3动画特效
纸屑飘落生日蛋糕CSS3动画特效是一款js+css3制作的全屏纸屑飘落,生日蛋糕点亮庆祝动画特效。
59 3
|
3月前
|
JavaScript 前端开发 API
Vue.js 3:深入探索组合式API的实践与应用
Vue.js 3:深入探索组合式API的实践与应用
|
3月前
|
JavaScript 前端开发 API
深入理解Node.js事件循环及其在后端开发中的应用
本文旨在揭示Node.js的核心特性之一——事件循环,并探讨其对后端开发实践的深远影响。通过剖析事件循环的工作原理和关键组件,我们不仅能够更好地理解Node.js的非阻塞I/O模型,还能学会如何优化我们的后端应用以提高性能和响应能力。文章将结合实例分析事件循环在处理大量并发请求时的优势,以及如何避免常见的编程陷阱,从而为读者提供从理论到实践的全面指导。
|
3月前
|
JavaScript
如何使用内存快照分析工具来分析Node.js应用的内存问题?
需要注意的是,不同的内存快照分析工具可能具有不同的功能和操作方式,在使用时需要根据具体工具的说明和特点进行灵活运用。
76 3

热门文章

最新文章

  • 1
    当面试官再问我JS闭包时,我能答出来的都在这里了。
    45
  • 2
    【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
    29
  • 3
    Node.js 中实现多任务下载的并发控制策略
    34
  • 4
    【2025优雅草开源计划进行中01】-针对web前端开发初学者使用-优雅草科技官网-纯静态页面html+css+JavaScript可直接下载使用-开源-首页为优雅草吴银满工程师原创-优雅草卓伊凡发布
    26
  • 5
    【JavaScript】深入理解 let、var 和 const
    49
  • 6
    【04】Java+若依+vue.js技术栈实现钱包积分管理系统项目-若依框架二次开发准备工作-以及建立初步后端目录菜单列-优雅草卓伊凡商业项目实战
    47
  • 7
    【03】Java+若依+vue.js技术栈实现钱包积分管理系统项目-若依框架搭建-服务端-后台管理-整体搭建-优雅草卓伊凡商业项目实战
    57
  • 8
    【02】Java+若依+vue.js技术栈实现钱包积分管理系统项目-商业级电玩城积分系统商业项目实战-ui设计图figmaUI设计准备-figma汉化插件-mysql数据库设计-优雅草卓伊凡商业项目实战
    57
  • 9
    如何通过pm2以cluster模式多进程部署next.js(包括docker下的部署)
    72
  • 10
    【01】Java+若依+vue.js技术栈实现钱包积分管理系统项目-商业级电玩城积分系统商业项目实战-需求改为思维导图-设计数据库-确定基础架构和设计-优雅草卓伊凡商业项目实战
    55