Javascript-this/作用域/闭包

news/2024/7/1 16:52:15/文章来源:https://www.cnblogs.com/newBuLashi/p/18272394

this & 作用域 & 闭包

this的核心,在大多数情况下,可以简单地理解为谁调用了函数,this就指向谁。但请注意,这里不包括通过callapplybindnew操作符或箭头函数进行调用的特殊情况。在这些特殊情况下,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

你看明白了吗?

image-20240628002235481

那提问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())

image-20240628003040224

// 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());

image-20240628003313572

call & apply & bind

  • 相同点:三者都是用来改变函数调用时的this指向
  • 不同:
    • call functionName.call(thisArg, arg1, arg2, ...) 参数需要一个一个传递
    • apply functionName.apply(thisArg, [argsArray]) 参数可以用数组传递
    • bind
      • functionName.bind(thisArg[, arg1[, arg2[, ...]]])
      • 返回一个新函数,在bind()被调用时,这个新函数this被指定为bind()的第一个参数,而其余参数将作为新函数的参数供调用时使用。
  • callapply 都会调用函数,并返回函数调用的结果(如果有的话)。
  • bind 不会调用函数,而是返回一个新的函数,这个新函数在被调用时才会执行原始函数,并且具有指定的this值和预置的参数。
  • 使用场景
    • 如果知道要传递的参数,并且想要立即调用函数,那么可以使用callapply
    • 如果想要创建一个新函数,这个函数在被调用时具有特定的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 是被调用的函数,所以 thismyCall 内部指向 speak 函数。而 context 是我们传递给 myCallobj1 对象,我们希望在 speak 函数内部使用 obj1 作为 this 上下文。因此,我们将 speak 函数作为 obj1 的一个方法(临时)来调用它,实现了改变 this 上下文的效果。

中途打断点可以看到context的值如图

image-20240627234537458

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]

在这个例子中,listWithItemslist 函数的一个新版本,将 '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时传入的参数。

image-20240628010304262

当我们使用 bind 方法(无论是原生的 Function.prototype.bind 还是手写实现的 myBind)时,我们实际上是在创建一个新的函数,这个函数被“绑定”到了特定的 this 上下文(在这个例子中是 obj 对象)以及一些预先设定的参数(在这个例子中是 'Hello')。

这个新函数(我们称之为 boundHello)现在可以独立使用,并且每次调用它时,都会以我们指定的 this 上下文(obj)和预先设定的参数('Hello')来调用原始的 hello 函数。

作用域和闭包 后续补上

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/732504.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

TIM1比较模式

暂不讨论捕获功能,简化下面的描述方便阅读理解。 -------------------------------------------------------------------------------- 13.4.14 TIM1 比较寄存器 1(TIMx_CCR1)    CCR1[15:0]: 比较通道1的值 (Compare 1 value) 若CC1 通道配置为输出:CCR1包含了装入比较…

[C++ Primer] 顺序容器

记录C++中STL标准模板库的常见操作顺序容器 顺序容器概述下标列出了标准库中的顺序容器,所有顺序容器都提供了快速顺序访问元素的能力。这些容器在以下方面都有不同的性能折中:向容器添加或从容器中删除元素的代价 非顺序访问容器中元素的代价2. 若不确定应该使用哪种容器,可…

Centos7.6下安装配置Jenkins

目前,网上很多关于Jenkins持续集成工具在Centos上部署教程,但发现部署完之后,基本都是不能使用,大部分是关于版本问题的报错, 都会提示让我们更新jenkins到比较新的版本,而更新到新版Jenkins又必须使用JDK11以上的版本,而我开发、测试用的环境JDK8版本, 一旦升级JDK,很…

VSCode 中 Markdown Preview Enhanced 插件利用 Chrome (Puppeteer) 导出 PDF 文件使用说明与问题解决

准备 预先安装好 Chrome 浏览器。 使用方法 右键选择 Chrome (Puppeteer)。设置 Puppeteer 通过 front-matter 即在 markdown 文档开头加上 yaml 格式的配置代码 --- puppeteer:format: "A4"scale: 1.0margin:top: 2cmright: 3cmbottom: 2cmleft: 3cm ---这里 format…

全网最适合入门的面向对象编程教程:03 类和对象的Python实现-为自定义类添加属性

本文主要介绍了,当使用Python创建自定义类时,如何为其添加属性,包括为类和实例添加属性两种,以及如何获取自定义的属性等内容。摘要: 本文主要介绍了,当使用 Python 创建自定义类时,如何为其添加属性,包括为类和实例添加属性两种,以及如何获取自定义的属性等内容。 往…

全网最适合入门的面向对象编程教程:03 类和对象的Python实现-使用Python创建类

本文主要介绍了,当使用Python创建自定义类时,如何为其添加属性,包括为类和实例添加属性两种,以及如何获取自定义的属性等内容。摘要: 本文主要介绍了,当使用 Python 创建自定义类时,如何为其添加属性,包括为类和实例添加属性两种,以及如何获取自定义的属性等内容。 往…

Centos7下安装配置MySQL5.7

本次安装的版本是在Centos7.6版本下安装配置mysql5.7版本。具体操作步骤如下: 1. 首先通过wget命令,下载MySQL源安装包: wget https://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm 安装MySQL源: yum localinstall mysql57-community-release-el7-11.noar…

Hugging Face Accelerate 两个后端的故事:FSDP 与 DeepSpeed

社区中有两个流行的 零冗余优化器(Zero Redundancy Optimizer,ZeRO) 算法实现,一个来自 DeepSpeed,另一个来自 PyTorch。Hugging Face Accelerate 对这两者都进行了集成并通过接口暴露出来,以供最终用户在训练/微调模型时自主选择其中之一。本文重点介绍了 Accelerate 对…

自制键盘(一)

概述 这个项目的灵感来源是稚晖君的客制化键盘项目,看到瀚文就觉得很震撼,就想自己也做一把试试。正好最近也正好在学嵌入式,顺便巩固一下最近所学的知识、也可以多了解一点技术栈。不过由于技术首先所以先做一把小键盘试试手。 项目整体思路 整个项目大致可以分为三个部分,…

尝试使用 Python 截屏并录屏

( 本文的完整版地址在 https://www.ccgxk.com/?post=494 ) 我在去年,曾经尝试过一个大胆的东西,就是使用 Python 写了个程序来录屏,以此给自己一种“期待感”,当时有没有效果我忘了,但是现在我又将这个项目捡了起来。界面是长上面那个样子,集成了项目名设置、开始录…

Compose 延迟列表踩过的坑

问题 在使用 Jetpack Compose 延迟列表时遇到一个坑,简单记录一下。直接上代码:这个代码看起来也没有什么问题,滑动正常,点击滑动到顶部也正常。 但是极端操作:在一边滑动列表一边点击按钮,就出问题了。这样再点击按钮,就不生效了。从日志来看,点击时协程发射值没有问题…

代码随想录算法训练营第22天 | 77.组合 216.组合总和 17.电话号码的字母组合

77.组合 给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。 解题 只能取比它大的,所以有个参数startindex 参数:一维数组单个组合path,二维数组结果集result,总数n,组合大小k,搜索结果的开始索引startindex 终止条件:path.size=k点击查看代码 class Solut…

(四)详解RLHF

一直都特别好奇大模型的强化学习微调是怎么做的,网上虽然相关文章不少,但找到的文章都是浅尝辄止说到用PPO训练,再细致深入的就没有讲了。。。只能自己看一看代码,以前搞过一点用PPO做游戏,感觉和语言模型PPO的用法不太一样。在游戏场景,每个step给环境一个action之后,a…

模拟集成电路设计系列博客——8.1.1 锁相环基本介绍

8.1.1 锁相环基本介绍 几乎所有的数字,射频电路以及大部分的模拟电路。不幸的是,集成电路振荡器本身并不适合用于高性能电路中的频率/时间参考源。一个主要的问题是它们的震荡频率并不能精确知道。更进一步的,集成电路振荡器的时钟抖动(可以被认为是频率上的随机波动)对于…

(三)使用 PPO 算法进行 RLHF 的 N 步实现细节

使用 PPO 算法进行 RLHF 的 N 步实现细节 当下,RLHF/ChatGPT 已经变成了一个非常流行的话题。我们正在致力于更多有关 RLHF 的研究,这篇博客尝试复现 OpenAI 在 2019 年开源的原始 RLHF 代码库,其仓库位置位于 openai/lm-human-preferences。尽管它具有 “tensorflow-1.x” …

JMeter安装目录简单说明

一 前言 环境: window 10 JMeter5.3 JMeter安装目录的文件通常容易被忽略,注意力全放在JMeter本身的各个功能的使用上。 但在前面的学习中我们发现了熟悉安装目录的必要性。 如jmeter.properties这个文件,之前的文章中就经常查看或者修改,还有一些日志文件也在安装目录中 二…

G61【模板】线性基 P3812 线性基

视频链接: G23 线性方程组 高斯消元法 - 董晓 - 博客园 (cnblogs.com) P3812 【模板】线性基 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)// 线性基 O(63*n) #include <iostream> #include <cstring> #include <algorithm> using namespace std;typede…

寿司

寿司 题目描述解析 合法的结果只有两种情况:\(B\) 都在两边、\(R\) 都在两边,至于是最左边还是最右边或者都有,无所谓,因为是环。 而每个 \(B\) 移到最左边的代价就是它左边 \(R\) 的个数,移到最右边就是它右边 \(R\) 的个数。 按环形 dp 的套路,我们可以把串复制二倍,然…

(一)ChatGPT 背后的“功臣”——RLHF 技术详解

ChatGPT 背后的“功臣”——RLHF 技术详解 OpenAI 推出的 ChatGPT 对话模型掀起了新的 AI 热潮,它面对多种多样的问题对答如流,似乎已经打破了机器和人的边界。这一工作的背后是大型语言模型 (Large Language Model,LLM) 生成领域的新训练范式:RLHF (Reinforcement Learnin…