函数
什么是执行上下文
答案
执行上下文( execution context EC)是 JavaScript 代码在运行时所处的环境。每当函数被调用、代码块被执行,都会创建一个新的执行上下文。
-
执行上下文的作用
- 变量作用域管理 管理代码运行时的所有状态信息,涉及变量的声明、赋值、作用域等。
- 函数调用追踪 通过执行上下文栈(Call Stack)实现函数的嵌套调用和正确返回。
- 闭包实现基础 作用域链(Scope Chain)机制使得内层函数能访问外层变量。
this
绑定 在创建阶段确定this
的指向(全局/函数/Eval 上下文不同)。
-
执行上下文的机制
-
生命周期简述为三阶段
阶段 关键行为 创建阶段 处理声明等逻辑,执行环境的状态初始化 执行阶段 逐行执行代码,处理变量赋值、函数调用(触发新执行上下文入栈) 回收阶段 上下文出栈,内存被 GC 回收 -
核心组件
组件 目的 通用执行环境状态 code evaluation state
追踪代码执行状态,包括暂停和恢复执行。 Function
关联的函数对象(如果是函数执行环境),否则为 null
。Realm
代码访问 ECMAScript 资源的 Realm 记录。 ScriptOrModule
代码来源的 Script 记录或 Module 记录,初始化时可能为 null
。ECMAScript 代码执行环境额外状态 LexicalEnvironment
词法环境,存储作用域链,用于解析标识符。 VariableEnvironment
变量环境,存储 var
变量的绑定。PrivateEnvironment
私有环境,存储类的私有字段,若无则为 null
。生成器执行环境额外状态 Generator
关联的生成器对象,表示正在执行的 Generator。 -
其他关键点
- 执行环境栈:用于管理执行环境,当前运行的执行环境始终位于栈顶。
- 执行环境的切换:通常遵循 LIFO 规则(后进先出)
- 不可访问性:ECMAScript 代码无法直接访问或观察执行环境,它仅作为规范机制存在。
-
-
执行上下文的类型
类型 | 触发条件 | 特点 |
---|---|---|
全局执行上下文 | 脚本首次运行时创建 | this 指向 window (浏览器)/global (Node.js),VO 是全局对象 |
函数执行上下文 | 每次函数调用时创建 | this 由调用方式决定,AO 存储 arguments 和局部变量 |
Eval 执行上下文 | eval() 代码执行时 | 作用域取决于调用位置,严格模式下有独立作用域 |
执行上下文是 ECMAScriptExecutable Code and Execution Contexts 章节定义的抽象概念。实际开发中无法直接访问或观察执行上下文,引擎的实现也不一定会完全遵从该定义。但是通过深入理解了执行上下文的机制,可以辅助理解作用域链、闭包、this 等 JavaScript 核心语言特性
延伸阅读
- JavaScript Execution Context 详细讲解了执行上下文和环境记录的关系
- JavaScript Visualized - Execution Contexts 国外博主的一个讲解执行上下文的可视化视频,基于该视频可以更直观地理解执行上下文的概念
this
this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断。
-
第一种是
函数调用模式
,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。 -
第二种是
方法调用模式
,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。 -
第三种是
构造器调用模式
,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。 -
第四种是
apply 、 call 和 bind 调用模式
,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。
这四种方式,使用构造器调用模式的优先级最高,然后是 apply、call 和 bind 调用模式,然后是方法调用模式,然后是函数调用模式。
- this 是一个关键字,它的值取决于当前的执行环境,而非申明环境
- this 值的判定方法,参考 你不知道的 JavaScript 上卷-第二部分 this 和对象原型
- 默认绑定,当为普通函数调用或处于全局环境时
- 严格模式 this 为 undefined
- 非严格模式为 window
- 隐式绑定,当未对象成员的函数调用时,this 为当前对象 > 此种情况最易出错,当对象成员函数经过赋值等其他操作时,会转变为默认绑定须严格区分
- 显示绑定 当采用如下方法调用函数时会改变 this 的值
- call 修改运行时 this
- apply 效果同上,传入参数维数组模式
- bind 永久改变 this,返回新的函数 > 当 this 的值为 null,undefined 时会采用 默认绑定,若赋值为原始值则采用原始封装对象进行替换
- new 当采用 new 运行函数时,this 指向新创建的对象
- 箭头函数 this 继承外层执行环境的 this 值
- 默认绑定,当为普通函数调用或处于全局环境时
DOM 中的 this
当在回调中绑定 this 时,this 值的机制,由于是用户代理触发回调执行,this 的值等于申明时绑定的 dom 环境。
const obj = {
name: 'yanle',
age: 20,
getName: () => {
const _getName = () => {
console.log('this.getName', this.name)
}
_getName()
},
getAge: function () {
const _getAge = () => {
console.log('this.getAge', this.age)
}
_getAge()
},
extend: {
name: 'le',
age: 20,
getName: function () {
console.log('name: ', this.name)
},
getAge: () => {
console.log('age: ', this.age)
}
}
}
obj.getName()
obj.getAge()
obj.extend.getName()
obj.extend.getAge()
obj.extend.getName.bind(obj)()
obj.extend.getAge.bind(obj)()
执行结果
this.getName undefined
this.getAge 20
name: le
age: undefined
name: yanle
age: undefined
解释如下:
obj.getName()
:在箭头函数getName中,this指向的是全局对象(在浏览器中是window对象,Node.js 中是Global对象)。因此this.getName输出undefined。obj.getAge()
:在普通函数getAge中,this指向的是obj对象。因此this.getAge输出20。obj.extend.getName()
:在普通函数getName中,this指向的是obj.extend对象。因此this.name输出le。obj.extend.getAge()
:在箭头函数getAge中,this指向的是全局对象(在浏览器中是window对象,Node.js 中是Global对象)。因此this.age输出undefined。obj.extend.getName.bind(obj)()
:通过bind方法将getName函数绑定到obj对象上,并立即调用绑定后的函数。在绑定后调用时,this指向的是obj对象。因此this.name输出yanle。obj.extend.getAge.bind(obj)()
:在箭头函数 getAge 中,this 是在函数定义时绑定的,而不是在函数调用时绑定的。在这种情况下,箭头函数的 this 指向的是外层作用域的 this,即全局对象(在浏览器中是 window 对象,Node.js 中是 Global 对象)。因此,在 obj.extend.getAge.bind(obj)() 中,this.age 输出的是全局对象的 age,而全局对象中并没有定义 age 属性,所以结果是 undefined。
JavaScript 中 this 指向混乱的原因主要有以下几个:
-
函数调用方式不同:JavaScript 中函数的调用方式决定了 this 的指向。常见的函数调用方式有函数调用、方法调用、构造函数调用和箭头函数调用。不同的调用方式会导致 this 指向不同的对象,容易引发混乱。
-
丢失绑定:当函数作为一个独立的变量传递时,或者作为回调函数传递给其他函数时,函数内部的 this 可能会丢失绑定。这意味着函数中的 this 不再指向原来的对象,而是指向全局对象(在浏览器环境中通常是 window 对象)或 undefined(在严格模式下)。
-
嵌套函数:当函数嵌套在其他函数内部时,嵌套函数中的 this 通常会与外部函数的 this 不同。这可能导致 this 的指向出现混乱,特别是在多层嵌套的情况下。
-
使用 apply、call 或 bind 方法:apply、call 和 bind 是 JavaScript 中用于显式指定函数的 this 的方法。如果不正确使用这些方法,比如传递了错误的上下文对象,就会导致 this 指向错误。
-
箭头函数:箭头函数具有词法作用域的 this 绑定,它会捕获其所在上下文的 this 值,而不是动态绑定 this。因此,在箭头函数中使用 this 时,它指向的是箭头函数声明时的上下文,而不是调用时的上下文。
为了避免 this 指向混乱的问题,可以采取以下措施:
- 使用箭头函数,确保 this 始终指向期望的上下文。
- 在函数调用时,确保正确设置了函数的上下文对象,可以使用 bind、call 或 apply 方法。
- 使用严格模式,避免函数内部的 this 默认绑定到全局对象。
- 在嵌套函数中,使用箭头函数或者显式保存外部函数的 this 值,以避免内部函数的 this 指向错误。
理解和正确处理 this 的指向是 JavaScript 开发中重要的一环,它能帮助我们避免许多常见的错误和混乱。
关于 this 指针的研究
础实例说明
实例1:
<script>
var name = "Kevin Yang";
function sayHi(){
console.log("你好,我的名字叫" + this.name);
}
sayHi()
</script>
如果在html 端, 这个this.name 是可以调用全局对象name的, 这个this实际上是指向的window的, var 也是把变量挂在到window对象上面的。
但是同样的这个实例如果放在node 端,就是一个undefined ,原因是node端没有window对象。
实例2:
const name = 'Kevin Yang'
function sayHi () {
console.log('你好,我的名字叫' + this.name)
}
const person = {}
person.sayHello = sayHi
person.sayHello()
这一次打招呼的内容就有点无厘头了,我们发现this.name已经变成undefined了。这说明,在sayHello函数内部执行时已经找不着this.name对象了。,原因是这儿时候,this指向的person 对象,但是this对象上面是没有name属性的。
如果改为这样 var person = {name:"Marry"};
就可以得到我们想要的内容了。
别this指针的指导性原则
在Javascript里面,this指针代表的是执行当前代码的对象的所有者。
在上面的示例中我们可以看到,第一次,我们定义了一个全局函数对象sayHi并执行了这个函数,函数内部使用了this关键字, 那么执行this这 行代码的对象是sayHi(一切皆对象的体现),sayHi是被定义在全局作用域中。其实在Javascript中所谓的全局对象, 无非是定义在 window这个根对象下的一个属性而已。因此,sayHi的所有者是window对象。也就是说,在全局作用域下, 你可以通过直接使用name去引用这 个对象,你也可以通过window.name去引用同一个对象。因而this.name就可以翻译为window.name了。
再来看第二个this的示例。第一次,person里面没有name属性,因此弹 出的对话框就是this.name引用的就是undefined对象 (Javascript中所有只声明而没有定义的变量全都指向undefined对象); 而第二次我们在定义person的时候加了name属性了,那么this.name指向的自然就是我们定义的字符串了。
理解了上面所说的之后,我们将上面最后一段示例改造成面向对象式的代码。
const name = 'Kevin Yang'
function sayHi () {
console.log('你好,我的名字叫' + this.name)
}
function Person (name) {
this.name = name
}
Person.prototype.sayHello = sayHi
const marry = new Person('Marry')
marry.sayHello()
const kevin = new Person('Kevin')
kevin.sayHello()
易误用的情况
示例1——内联式绑定Dom元素的事件处理函数
<body>
<input id="btnTest" type="button" value="点击我" onclick="sayHi()">
<script type="text/JavaScript">
function sayHi(){
alert("当前点击的元素是" + this.tagName);
}
</script>
</body>
在此例代码中,我们绑定了button的点击事件,期望在弹出的对话框中打印出点击元素的标签名。但运行结果却是: 当前点击的元素是 undefined
也就是this指针并不是指向input元素。这是因为当使用内联式绑定Dom元素的事件处理函数时,实际上相当于执行了以下代码:
在这种情况下sayHi函数对象的所有权并没有发生转移,还是属于window所有。用上面的指导原则一套我们就很好理解为什么this.tagName是undefined了。
那么如果我们要引用元素本身怎么办呢? 我们知道,onclick函数是属于btnTest元素的,那么在此函数内部,this指针正是指向此Dom对象,于是我们只需要把this作为参数传入sayHi即可。
<input id="btnTest" type="button" value="点击我" onclick="sayHi(this)">
<script type="text/JavaScript">
function sayHi(el){
alert("当前点击的元素是" + el.tagName); }
</script>
等价代码如下:
<script type="text/JavaScript">
document.getElementById("btnTest").onclick = function(){ sayHi(this); }
</script>
示例2——临时变量导致的this指针丢失
<script type="text/JavaScript">
var Utility = {
decode:function(str){ return unescape(str); },
getCookie:function(key){
// ... 省略提取cookie字符串的代码
var value = "i%27m%20a%20cookie";
return this.decode(value);
}
};
console.log(Utility.getCookie("identity"))
</script>
一般都会自己封装一个Utility的类,然后将一些常用的函数作为Utility类的属性,如客户端经常会 用到的getCookie函数和解码函数。 如果每个函数都是彼此独立的,那么还好办,问题是,函数之间有时候会相互引用。例如上面的getCookie函 数, 会对从document.cookie中提取到的字符串进行decode之后再返回。如果我们通过Utility.getCookie去调用的话,那 么没有问题, 我们知道,getCookie内部的this指针指向的还是Utility对象,而Utility对象时包含decode属性的。代码可以成 功执行。
但是有个人不小心这样使用Utility对象呢?
<script type="text/JavaScript">
function showUserIdentity(){
// 保存getCookie函数到一个局部变量,因为下面会经常用到
var getCookie = Utility.getCookie;
alert(getCookie("identity"));
}
showUserIdentity();
</script>
这个时候运行代码会抛出异常“this.decode is not a function”。 运用上面我们讲到的指导原则,很好理解,因为此时Utility.getCookie对象被赋给了临时变量getCookie, 而临 时变量是属于window对象的——只不过外界不能直接引用,只对Javascript引擎可见——于是在getCookie函数内部的this指针指向 的就是window对象了, 而window对象没有定义一个decode的函数对象,因此就会抛出这样的异常来。
这个问题是由于引入了临时变量导致的this指针的转移。解决此问题的办法有几个: 不引入临时变量,每次使用均使用Utility.getCookie进行调用 getCookie函数内部使用Utility.decode显式引用decode对象而不通过this指针隐式引用(如果Utility是一个实例化的对象,也即是通过new生成的,那么此法不可用) 使用Funtion.apply或者Function.call函数指定this指针
第三种使用apply 和 call 修正的办法实例如下:
<script type="text/JavaScript">
function showUserIdentity(){
// 保存getCookie函数到一个局部变量,因为下面会经常用到
var getCookie = Utility.getCookie;
alert(getCookie.call(Utility,"identity"));
alert(getCookie.apply(Utility,["identity"]));
}
showUserIdentity();
</script>
示例3——函数传参时导致的this指针丢失
<script type="text/JavaScript">
var person = {
name:"Kevin Yang",
sayHi:function(){
alert("你好,我是"+this.name);
}
}
setTimeout(person.sayHi,5000);
</script>
这段代码期望在访客进入页面5秒钟之后向访客打声招呼。setTimeout函数接收一个函数作为参数,并在指定的触发时刻执行这个函数。 可是,当我们等了5秒钟之后,弹出的对话框显示的this.name却是undefined。
其实这个问题和上一个示例中的问题是类似的,都是因为临时变量而导致的问题。 当我们执行函数的时候,如果函数带有参数,那么这个时候Javascript引擎会创建一个临时变量, 并将传入的参数复制(注意,Javascript里面都是值传递的,没有引用传递的概念)给此临时变量。 也就是说,整个过程就跟上面我们定义了一个getCookie的临时变量,再将Utility.getCookie赋值给这个临时变量一样。只不过在这个示例中,容易忽视临时变量导致的bug。
数对象传参
Prototype的解决方案——传参之前使用bind方法将函数封装起来,并返回封装后的对象
<script type="text/JavaScript">
var person = {
name:"Kevin Yang",
sayHi:function(){
alert("你好,我是"+this.name);
}
}
var boundFunc = person.sayHi.bind(person,person.sayHi);
setTimeout(boundFunc,5000);
</script>
bind方法的实现其实是用到了Javascript又一个高级特性——闭包。我们来看一下源代码:
function bind () {
if (arguments.length < 2 && arguments[0] === undefined) { return this }
const __method = this; const args = $A(arguments); const object = args.shift()
return function () { return __method.apply(object, args.concat($A(arguments))) }
}
首先将this指针存入函数内部临时变量,然后在返回的函数对象中引用此临时变量从而形成闭包。
化的this
在JavaScript中,this通常 指向的是我们正在执行的函数本身,或者是指向该函数所属的对象(运行时)。 当我们在页面中定义了函数 doSomething()的时候,它的owner是页面,或者是JavaScript中的window对象(或 global对象)。 对于一个onclick属性,它为它所属的HTML元素所拥有,this应该指向该HTML元素。
在几种常见场景中this的变化
function doSomething () {
alert(this.navigator) // appCodeName
this.value = 'I am from the Object constructor'
this.style.backgroundColor = '# 000000'
}
- 作为普通函数直接调用时,this指向window对象.
- 作为控件事件触发时
- inline event registration 内联事件注册 .将事件直接写在HTML代码中
(<element onclick=”doSomething()”>)
, 此时this指向 window对象 。 - Traditional event registration 传统事件注册 (DHTML方式). 形如 element.onclick = doSomething; 此时this指向 element对象
<element onclick=”doSomething(this)”>
作为参数传递可以指向element- 作为对象使用时this指向当前对象。形如:new doSomething();
- 使用apply 或者call方法时,this指向所传递的对象。 形如:var obj=; doSomething.apply(obj,new Array(”nothing”));
下来文章中我们将要讨论的问题是:在函数doSomething()中this所指的是什么?
function doSomething () {
this.style.color = '#cc0000'
}
在 JavaScript中,this通常指向的是我们正在执行的函数本身(译者注:用owner代表this所指向的内容),或者是,指向该函数所属的对 象。
当我们在页面中定义了函数doSomething()的时候,它的owner是页面,或者是JavaScript中的window对象(或 global对象)。
对于一个onclick属性,它为它所属的HTML元素所拥有,this应该指向该HTML元素。
这种“所有权”就是JavaScript中面向对象的一种方式。在Objects as associative arrays中可以查看一些更多的信息。
怎样在一个代码环境中快速的找到this所指的对象呢?
- 1、 要清楚的知道对于函数的每一步操作是拷贝还是引用(调用)
- 2、 要清楚的知道函数的拥有者(owner)是什么
- 3、 对于一个function,我们要搞清楚我们是把它当作函数使用还是在当作类使用
变量提升
变量提升是 js 语言的一种机制确保在变量或函数定义后,无需考虑调用位置对变量或函数进行引用。
它具有如下机制:
- 变量或函数声明语句会在代码执行前,完成对变量或函数的初始化
- 同名函数和变量申明,变量申明提升在前
- 重复申明的函数提升会按照声明顺序后面覆盖前面
- let 提升会出现 TDZ 现象
scope
JavaScript 作用域链(Scope Chain)是指变量和函数的可访问性和查找规则。它是由多个执行上下文(Execution Context)的变量对象(Variable Object)按照它们被创建的顺序组成的链式结构。
在 JavaScript 中,每个函数都会创建一个新的执行上下文,并将其添加到作用域链的最前端。当访问一个变量时,JavaScript 引擎会先从当前执行上下文的变量对象开始查找,如果找不到,则沿着作用域链依次向上查找,直到全局执行上下文的变量对象。
作用域链的创建过程如下:
- 在函数定义时,会创建一个变量对象(VO)来存储函数的变量和函数声明。这个变量对象包含了当前函数的作用域中的变量和函数。
- 在函数执行时,会创建一个执行上下文(Execution Context),并将其添加到作用域链的最前端。执行上下文中的变量对象称为活动对象(Active Object)。
- 当访问一个变量时,JavaScript 引擎首先会在活动对象中查找,如果找不到,则沿着作用域链依次向上查找,直到全局执行上下文的变量对象。
- 如果在作用域链的任何一个环节找到了变量,则停止查找并返回变量的值;如果未找到,则抛出引用错误(ReferenceError)。
作用域链的特点:
- 作用域链是一个静态的概念,它在函数定义时就确定了,不会随着函数的调用而改变。
- 作用域链是由多个执行上下文的变量对象按照它们被创建的顺序组成的。
- 作用域链的最后一个变量对象是全局执行上下文的变量对象,它是作用域链的终点。
- 内部函数可以访问外部函数的变量,因为内部函数的作用域链包含了外部函数的变量对象。
有哪些应用场景
作用域链在 JavaScript 中具有广泛的应用场景。下面列举了一些常见的应用场景:
-
变量查找:作用域链决定了变量的访问顺序,当访问一个变量时,会按照作用域链的顺序依次查找变量,直到找到匹配的变量或到达全局作用域。
-
闭包:闭包是指函数能够访问和操作它的外部函数中定义的变量。通过作用域链,内部函数可以访问外部函数的变量,实现了闭包的特性。闭包在许多场景中用于创建私有变量和实现函数封装。
-
垃圾回收:JavaScript 的垃圾回收机制通过作用域链来判断变量的生命周期。当变量不再被引用时,垃圾回收器可以回收它所占用的内存空间。
-
函数作为参数传递:在 JavaScript 中,可以将函数作为参数传递给其他函数。在传递过程中,作用域链决定了内部函数对外部函数变量的访问权限,实现了回调函数和高阶函数的功能。
-
面向对象编程:JavaScript 中的对象和原型链是基于作用域链实现的。通过原型链,对象可以访问和继承其原型对象的属性和方法。
-
模块化开发:作用域链可以用于实现模块化开发,通过定义私有变量和公共接口,控制模块内部变量的可访问性,避免变量冲突和全局污染。
-
作用域链的动态改变:在 JavaScript 中,可以通过闭包和动态作用域的特性来改变作用域链。例如,使用 eval() 函数或 with 语句可以改变当前的作用域链。
总之,作用域链在 JavaScript 中扮演了重要的角色,涵盖了变量的访问、闭包、垃圾回收、模块化开发等多个方面。深入理解作用域链对于编写高质量的 JavaScript 代码和理解其底层工作原理非常重要。
闭包
闭包就是在函数的词法作用域外访问函数内作用域的现象。
闭包的原理是由于外部变量持有内层函数的引用导致函数及其变量未被释放仍就可用
- 闭包保留了内部函数所有的状态
使用场景
- 私有变量
- 回调模式保存动画状态,相比全局变量每个动画可以维持自己的状态值。避免全局污染。
闭包在 JavaScript 中有很多实用的使用场景,以下是一些主要的场景:
一、数据隐藏和封装
- 保护变量:
- 闭包可以创建一个私有作用域,将变量封装在函数内部,防止外部直接访问和修改。只有通过特定的函数接口才能访问和操作这些变量。
- 例如:
function createCounter () {
let count = 0
return {
increment () {
count++
},
getCount () {
return count
}
}
}
const counter = createCounter()
counter.increment()
console.log(counter.getCount()) // 1
- 在这个例子中,
count
变量被封装在createCounter
函数内部,外部无法直接访问,只能通过返回的对象上的方法来操作count
。
- 模拟私有方法:
- 在面向对象编程中,可以使用闭包来模拟私有方法。私有方法只能在对象内部被访问,外部无法直接调用。
- 例如:
const myObject = (function () {
let privateVariable = 0
function privateMethod () {
privateVariable++
console.log(privateVariable)
}
return {
publicMethod () {
privateMethod()
}
}
})()
myObject.publicMethod() // 1
- 在这个例子中,
privateMethod
和privateVariable
只能在内部函数中被访问,外部通过调用publicMethod
间接访问了私有方法。
二、函数柯里化(Currying)
- 逐步参数化:
- 闭包可以用于实现函数柯里化,将一个多参数的函数转换为一系列单参数的函数。每次调用只接受一部分参数,并返回一个新的函数,直到所有参数都被提供。
- 例如:
function add (a) {
return function (b) {
return function (c) {
return a + b + c
}
}
}
const addFiveAndSixAndSeven = add(5)(6)(7)
console.log(addFiveAndSixAndSeven) // 18
- 在这个例子中,
add
函数通过闭包逐步接受参数,最后返回一个计算结果。
- 灵活的参数传递:
- 函数柯里化可以使函数的参数传递更加灵活,特别是在需要部分应用参数或者延迟参数传递的情况下。
- 例如,可以先创建一个部分应用参数的函数,然后在需要的时候再传递剩余的参数。
三、回调函数和事件处理
- 保存外部环境:
- 在异步编程或者事件处理中,闭包可以保存外部函数的变量和状态,使得回调函数能够访问这些信息。
- 例如:
function setTimeoutWithMessage (message) {
setTimeout(function () {
console.log(message)
}, 1000)
}
setTimeoutWithMessage('Hello after 1 second!')
- 在这个例子中,回调函数内部的
message
变量是通过闭包从外部函数中获取的,即使外部函数已经执行完毕,回调函数仍然能够访问到这个变量。
- 事件处理程序:
- 在 DOM 事件处理中,闭包可以用于保存与事件相关的状态和数据。
- 例如:
<button id="myButton">Click me</button>
<script>
document.getElementById("myButton").addEventListener("click", function () {
const buttonText = this.textContent;
console.log(`Button clicked: ${buttonText}`);
});
</script>
- 在这个例子中,事件处理程序内部的
buttonText
变量是通过闭包从外部环境中获取的,每次点击按钮时,都能正确地打印出按钮的文本内容。
四、记忆化(Memoization)
- 缓存计算结果:
- 闭包可以用于实现记忆化,将函数的计算结果缓存起来,避免重复计算。如果相同的参数再次被传入,直接返回缓存的结果,而不是重新计算。
- 例如:
function memoizedAdd () {
const cache = {}
return function (a, b) {
const key = `${a},${b}`
if (cache[key]) {
return cache[key]
} else {
const result = a + b
cache[key] = result
return result
}
}
}
const memoizedAddFunction = memoizedAdd()
console.log(memoizedAddFunction(2, 3)) // 5
console.log(memoizedAddFunction(2, 3)) // 5(直接从缓存中获取结果)
- 在这个例子中,
memoizedAdd
函数内部的cache
对象用于缓存计算结果,通过闭包保存了这个缓存对象,使得每次调用函数时都能访问到它。
- 提高性能:
- 对于计算复杂或者频繁调用的函数,记忆化可以显著提高性能,减少不必要的计算。
1.1、什么是闭包
闭包,官方对闭包的解释是:一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。闭包的特点:
- 作为一个函数变量的一个引用,当函数返回时,其处于激活状态。
- 一个闭包就是当一个函数返回时,一个没有释放资源的栈区。
简单的说,Javascript允许使用内部函数---即函数定义和函数表达式位于另一个函数的函数体内。 而且,这些内部函数可以访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。 当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包。
1.2、闭包的几种写法和用法
JavaScript中闭包的应用使用闭包需要注意的地方:闭包使得函数中的变量都保存在内存中,内训消耗大,IE中有可能导致内存泄漏在父函数外部改变父函数内部变量的值。
第一种写法:
// 第1种写法
function Circle (r) {
this.r = r
}
Circle.PI = 3.14159
Circle.prototype.area = function () {
return Circle.PIthis.rthis.r
}
const c = new Circle(1.0)
alert(c.area())
第二种写法:
// 第2种写法
const Circle = function () {
// eslint-disable-next-line
const obj = new Object()
obj.PI = 3.14159
obj.area = function (r) {
return this.PIrr
}
return obj
}
const c = new Circle()
alert(c.area(1.0))
第三种写法:
// 第3种写法
// eslint-disable-next-line
const Circle = new Object()
Circle.PI = 3.14159
Circle.Area = function (r) {
return this.PIrr
}
alert(Circle.Area(1.0))
第四种写法:
// 第4种写法
const Circle = {
PI: 3.14159,
area: function (r) {
return this.PIrr
}
}
alert(Circle.area(1.0))
第五种写法:
// 第5种写法
// eslint-disable-next-line
const Circle = new Function('this.PI = 3.14159;this.area = function( r ) {return r*r*this.PI;}')
alert((new Circle()).area(1.0))
基础用法: 示例1:解决作用域问题
function f1 () {
let n = 1
// eslint-disable-next-line
test = function () {
n += 1
}
function f2 () {
console.log('f2():', n)
}
return f2
}
const res = f1() // 初始化f1()
console.log(res()) // 相当于调用f2(),结果1和undefined
// eslint-disable-next-line
test() // 将n的值改变了
console.log(res()) // 结果2和undefined
示例2:实现get 和 set
let setValue, getValue;
(function () {
let n = 0
getValue = function () {
return n
}
setValue = function (x) {
n = x
}
})()
// console.log(n); n is not defined
console.log(getValue())
setValue(567)
console.log(getValue())
示例3:用闭包实现迭代器的效果
// 迭代器中得应用
function test (x) {
let i = 0
return function () {
return x[i++]
}
}
// eslint-disable-next-line
const next = test(['a', 'b', 'c', 'd'])
console.log(next())
console.log(next())
console.log(next())
console.log(next()) // 每调用一次,都可以将数组指针向下移动一次
示例4: 错误的示范:
function f () {
const a = []
let i
for (i = 0; i < 3; i++) {
a[i] = function () {
return i
}
}
return a
}
const test = f()
console.log(test[0]())
console.log(test[1]())
console.log(test[2]()) // 结果都是 3 3 3 这种写法是错误的
正确的示范:
function f () {
const a = []
let i
for (i = 0; i < 3; i++) {
a[i] = (function (x) {
return function () {
return x
}
})(i)
}
return a
}
const test = f()
console.log(test[0]())
console.log(test[1]())
console.log(test[2]())
示例5:对示例4的优化
function f () {
function test (x) {
return function () {
return x
}
}
const a = []
let i
for (i = 0; i < 3; i++) {
a[i] = test(i)
}
return a
}
const res = f()
alert(res[0]())
alert(res[1]())
alert(res[2]())
1.3、关于prototype的一些理解
上面代码中出现了JS中常用的Prototype,那么Prototype有什么用呢?下面我们来看一下:
const dom = function () {
}
dom.Show = function () {
alert('Show Message')
}
dom.prototype.Display = function () {
alert('Property Message')
}
dom.Display() // error
dom.Show()
// eslint-disable-next-line
const d = new dom()
d.Display()
d.Show() // error
我们首先声明一个变量,将一个函数赋给他,因为在Javascript中每个函数都有一个Portotype属性,而对象没有。添加两个方法,分别直接添加和添加打破Prototype上面,来看下调用情况。分析结果如下: 1、不使用prototype属性定义的对象方法,是静态方法,只能直接用类名进行调用!另外,此静态方法中无法使用this变量来调用对象其他的属性! 2、使用prototype属性定义的对象方法,是非静态方法,只有在实例化后才能使用!其方法内部可以this来引用对象自身中的其他属性!
下面我们再来看一段代码:
const dom = function () {
const Name = 'Default'
this.Sex = 'Boy'
this.success = function () {
alert('Success')
}
}
alert(dom.Name)
alert(dom.Sex)
大家先看看,会显示什么呢? 答案是两个都显示Undefined,为什么呢?这是由于在Javascript中每个function都会形成一个作用域,而这些变量声明在函数中, 所以就处于这个函数的作用域中,外部是无法访问的。要想访问变量,就必须new一个实例出来。
const html = {
Name: 'Object',
Success: function () {
this.Say = function () {
alert('Hello,world')
}
alert('Obj Success')
}
}
再来看看这种写法,其实这是Javascript的一个"语法糖",这种写法相当于:
// eslint-disable-next-line
const html = new Object()
html.Name = 'Object'
html.Success = function () {
this.Say = function () {
alert('Hello,world')
}
}
alert('Obj Success')
变量html是一个对象,不是函数,所以没有Prototype属性,其方法也都是公有方法,html不能被实例化。 但是他可以作为值赋给其它变量,如var o = html; 我们可以这样使用它:
alert(html.Name)
html.Success()
说到这里,完了吗?细心的人会问,怎么访问Success方法中的Say方法呢?是html.Success.Say()吗? 当然不是,上面刚说过由于作用域的限制,是访问不到的。所以要用下面的方法访问:
var s = new html.Success()
s.Say()
// 还可以写到外面
html.Success.prototype.Show = function () {
alert('HaHa')
}
// eslint-disable-next-line
var s = new html.Success()
s.Show()
<div id="class02">二、Javascript闭包的用途</div>
1、匿名自执行函数
我们知道所有的变量,如果不加上var关键字,则默认的会添加到全局对象的属性上去,这样的临时变量加入全局对象有很多坏处,比如:别的函数可能误用这些变量; 造成全局对象过于庞大,影响访问速度(因为变量的取值是需要从原型链上遍历的)。 除了每次使用变量都是用var关键字外,我们在实际情况下经常遇到这样一种情况,即有的函数只需要执行一次,其内部变量无需维护, 比如UI的初始化,那么我们可以使用闭包:
const data = {
table: [],
tree: {}
};
(function (dm) {
for (let i = 0; i < dm.table.rows; i++) {
const row = dm.table.rows[i]
for (let j = 0; j < row.cells; i++) {
drawCell(i, j)
}
}
})(data)
我们创建了一个匿名的函数,并立即执行它,由于外部无法引用它内部的变量,因此在函数执行完后会立刻释放资源,关键是不污染全局对象。
2、结果缓存
我们开发中会碰到很多情况,设想我们有一个处理过程很耗时的函数对象,每次调用都会花费很长时间, 那么我们就需要将计算出来的值存储起来,当调用这个函数的时候,首先在缓存中查找,如果找不到,则进行计算,然后更新缓存并返回值,如果找到了,直接返回查找到的值即可。 闭包正是可以做到这一点,因为它不会释放外部的引用,从而函数内部的值可以得以保留。
const CachedSearchBox = (function () {
const cache = {}
const count = []
return {
attachSearchBox: function (dsid) {
if (dsid in cache) { // 如果结果在缓存中
return cache[dsid]// 直接返回缓存中的对象
}
const fsb = new uikit.webctrl.SearchBox(dsid)// 新建
cache[dsid] = fsb// 更新缓存
if (count.length > 100) { // 保正缓存的大小<=100
delete cache[count.shift()]
}
return fsb
},
clearSearchBox: function (dsid) {
if (dsid in cache) {
cache[dsid].clearSelection()
}
}
}
})()
CachedSearchBox.attachSearchBox('input')
这样我们在第二次调用的时候,就会从缓存中读取到该对象。
3、封装
const person = (function () {
// 变量作用域为函数内部,外部无法访问
let name = 'default'
return {
getName: function () {
return name
},
setName: function (newName) {
name = newName
}
}
}())
print(person.name)// 直接访问,结果为undefined
print(person.getName())
person.setName('abruzzi')
print(person.getName())
// 得到结果如下:
// undefined
// default
// abruzzi
4、实现类和继承
function Person () {
let name = 'default'
return {
getName: function () {
return name
},
setName: function (newName) {
name = newName
}
}
}
const p = new Person()
p.setName('Tom')
alert(p.getName())
const Jack = function () {}
// 继承自Person
Jack.prototype = new Person()
// 添加私有方法
Jack.prototype.Say = function () {
alert('Hello,my name is Jack')
}
const j = new Jack()
j.setName('Jack')
j.Say()
alert(j.getName())
我们定义了Person,它就像一个类,我们new一个Person对象,访问它的方法。 下面我们定义了Jack,继承Person,并添加自己的方法。
new Function 了解多少?
基本概念
new Function()
是 JavaScript 中的一个构造函数,它可以实例化一个新的函数对象并返回。该函数对象可以使用传递给 new Function()
的字符串参数作为函数体,并使用其他传递给它的参数作为函数参数,从而动态创建一个可执行的函数。
具体来说,new Function()
构造函数可以接受多个字符串参数作为函数的参数和函数体,其参数形式如下:
new Function ([arg1[, arg2[, ...argn]],] functionBody)
其中,arg1, arg2, ..., argn
为函数的参数列表,functionBody
为函数体的字符串表示。当调用 new Function()
函数时,JavaScript 引擎会将 arg1, arg2, ..., argn
所表示的参数和 functionBody
所表示的函数体组合成一个新的函数对象,并将该对象返回。
举例
下面是一个简单的 new Function()
的使用示例,它使用 new Function()
构造函数动态创建一个函数对象,并将该对象作为变量 add
的值进行赋值:
const add = new Function('a', 'b', 'return a + b;');
console.log(add(2, 3)); // 5
上述代码中,new Function('a', 'b', 'return a + b;')
创建了一个新的函数对象,其中 'a'
和 'b'
是函数的参数列表,'return a + b;'
是函数的实现代码。然后,该函数对象被赋值给变量 add
。最后,调用 add(2, 3)
执行该函数,返回 5
。
需要注意的是,new Function()
构造函数不能访问其上下文中的变量和函数,因此在使用时需要特别注意作用域的限制。同时,由于 new Function()
构造函数的执行权限较为灵活,因此在使用时需要仔细检查并确保其输入参数的合法性和安全性。
new Function 和 eval 的区别
虽然 new Function()
和 eval()
都可以执行字符串形式的 JavaScript 代码,但是它们在执行方式、使用场景和安全性方面还是有很大的区别的。
下面是 new Function()
和 eval()
的主要区别:
-
执行方式不同:
new Function()
构造函数创建的函数对象只会在其被调用时才会执行,而eval()
函数则立即执行其参数中的 JavaScript 代码,并返回其中的值(如果有)。 -
作用域不同:
new Function()
构造函数创建的函数对象没有访问父作用域的能力,只能访问自己的局部变量和全局变量;而eval()
函数则可以访问其自身函数作用域和父作用域的变量和函数,因此具有更高的灵活性和不可预知性。 -
安全性不同:由于
new Function()
构造函数定义的函数对象是在严格的函数作用域下运行的,因此其代码不会改变或访问父作用域中的变量。因此,使用new Function()
构造函数创建函数对象时,可以更好地保证其安全性。而eval()
函数则无法保证代码的安全性,因为它可以访问并改变父作用域中的变量,从而具有更高的攻击风险。
new Function 性能
与 eval()
相比,new Function()
函数具有更好的性能。这是因为 new Function()
函数在编译时会创建一个新的函数对象,不会像 eval()
函数一样将代码注入到当前作用域中。相反,它只在需要时才编译并执行代码,因此在常规情况下,new Function()
的性能比 eval()
更好。
另外,由于 new Function()
在全局作用域外部定义新的函数,可以更好地掌控执行环境,因此我们可以利用 new Function()
函数的局部性,使其不仅取代 eval()
,而且在一定程度上比自执行函数和即时函数表达式引入更少的全局变量。
不过需要注意的是,如果在一个循环中频繁地使用 new Function()
,或者在函数体内创建过多的嵌套函数,可能会导致性能下降。因此,当需要使用 new Function()
函数时,应该尽量减少不必要的重复调用,并注意代码的优化和缓存。
new Function 使用场景
new Function()
的使用场景主要是动态生成 Javascript 代码的情况。由于它可以使用字符串形式作为函数体,并接受可变数量的参数,因此很适合在需要动态生成 JavaScript 代码的场景中使用。下面列举一些常见的使用场景:
-
动态生成函数:使用
new Function()
可以动态生成函数,有时候这种方式比使用函数表达式更加灵活。 -
模板引擎:某些模板引擎使用
new Function()
动态生成 JavaScript 代码来进行文本渲染和数据绑定。 -
解析 JSON:从服务端获取 JSON 数据时,可以使用
new Function()
将其转换为具有更好可读性的 JavaScript 对象。 举例:
const json = '{"name": "张三", "age": "18", "gender": "男"}'
// eslint-disable-next-line
const parseJson = new Function(`return ${json}`)
console.log(parseJson()) // 输出:{name: "张三", age: "18", gender: "男"}
- 在浏览器中查找或执行某些 DOM 元素:可以将 JavaScript 代码传递给
new Function()
进行动态执行和查找。
需要注意的是,由于 new Function()
可以动态生成和执行任意 JavaScript 代码,因此其安全性和风险需要仔细考虑和评估。在使用 new Function()
时,应该避免用于可疑的或不可信任的代码执行,并严格控制传递给函数构造函数的参数,以避免潜在的安全漏洞。
函数声明与函数表达式的区别
JavaScript中有两种主要的方式来定义函数:函数声明(Function Declaration)和函数表达式(Function Expression)。
- 函数声明(Function Declaration):
- 函数声明是通过使用
function
关键字后面跟着函数名称来创建的,通常位于作用域的顶部。 - 函数声明会被提升(Hoisting),即在执行代码之前就可以使用。这意味着可以在函数声明之前调用该函数。
- 函数声明创建的函数可以在整个作用域内部访问。
示例:
function sayHello () {
console.log('Hello!')
}
sayHello() // 可以在函数声明之后调用
- 函数表达式(Function Expression):
- 函数表达式是将函数赋值给变量或作为其他表达式的一部分创建的。
- 函数表达式通常是匿名函数,即没有指定函数名称。但也可以使用具名函数表达式,为函数表达式指定一个名称。
- 函数表达式不会被提升,必须在定义之后才能使用。
- 函数表达式创建的函数只能在其所在的变量或表达式作用域内访问。
示例:
// 匿名函数表达式
const sayHello = function () {
console.log('Hello!')
}
sayHello() // 必须在函数表达式之后调用
// 具名函数表达式
const add = function sum (a, b) {
return a + b
}
console.log(add(2, 3)) // 输出: 5
// console.log(sum(2, 3)); // 错误,无法在外部访问具名函数表达式的名称
总结:
- 函数声明是使用
function
关键字创建的函数,会被提升,可以在声明之前调用,而且在整个作用域内都可访问。 - 函数表达式是将函数赋值给变量或作为其他表达式的一部分创建的,不会被提升,必须在定义之后才能使用,且只能在其所在的变量或表达式作用域内访问。
匿名函数?
在JavaScript中,匿名函数是一种没有名称的函数。它是一种可以直接被定义和使用的函数,而不需要通过函数名进行引用。匿名函数通常用于需要临时定义一个函数并在某个地方立即调用它的情况下使用。
匿名函数可以使用两种方式进行定义:函数表达式和箭头函数。
- 函数表达式:
const func = function () {
// 函数的代码块
}
在上述代码中,我们定义了一个没有名称的函数,并将其赋值给了变量func
。这个函数可以通过func
变量进行调用。
- 箭头函数:
const func = () => {
// 函数的代码块
}
箭头函数是ES6引入的一种简化的函数表达式。它使用箭头(=>)来定义函数,并且没有自己的this值,继承了外层作用域的this值。
匿名函数常用于以下场景:
- 作为回调函数:匿名函数可以作为参数传递给其他函数,并在需要的时候被调用,例如事件处理函数、定时器回调等。
- 自执行函数:匿名函数可以在定义后立即调用,避免在全局作用域中定义过多的变量。
- 模块化开发:匿名函数可以用于封装私有变量和方法,实现模块化的开发和避免变量名冲突。
需要注意的是,由于匿名函数没有名字,所以在调试和异常追踪时可能会比较困难,因此在开发中建议给函数命名,以提高代码的可读性和可维护性。
追问: function 是匿名函数吗?
在JavaScript中,function
关键字用于定义函数,而不是匿名函数。function
关键字后面可以跟一个函数名,用于定义具名函数,也可以省略函数名,定义匿名函数。
具名函数示例:
function add (a, b) {
return a + b
}
上述代码中的add
函数是一个具名函数,可以通过函数名add
进行引用和调用。
匿名函数示例:
const sum = function (a, b) {
return a + b
}
上述代码中的sum
是一个匿名函数,它没有名称,但可以通过变量sum
进行引用和调用。
可以看到,具名函数和匿名函数的区别在于函数名的存在与否。具名函数可以在函数内部和外部通过函数名进行引用和调用,而匿名函数则需要通过赋值给变量或作为参数传递给其他函数来引用和调用。
需要注意的是,在使用函数表达式定义匿名函数时,函数名是可选的,但在使用函数声明定义具名函数时,函数名是必需的,且函数声明的语法要求将函数名和函数体写在一起。
箭头函数?
箭头函数和普通函数是 JavaScript 中两种不同的函数定义方式,它们有以下的区别:
-
语法不同:箭头函数使用箭头 => 来定义函数,而普通函数使用 function 关键字来定义函数。
-
箭头函数没有自己的 this,它会继承其所在作用域的 this 值。而普通函数的 this 则由函数调用时的上下文所决定,可以通过 call、apply、bind 方法来改变。
-
箭头函数没有自己的 arguments 对象,它可以通过 rest 参数语法来接收不定数量的参数。而普通函数则有自己的 arguments 对象,它可以接收任意数量的参数。
-
箭头函数不能作为构造函数使用,不能使用 new 来实例化,因为它没有自己的 this,而普通函数可以用 new 来创建新的对象。
-
箭头函数不能使用 yield 关键字来定义生成器函数,而普通函数可以。
-
箭头函数不支持call()/apply()函数特性
-
箭头函数没有prototype属性
-
原型函数不能定义成箭头函数 比如下面这个例子:
function Person (name) {
this.name = name
}
// 原型函数使用箭头函数,其中的this指向全局对象,而不会指向构造函数
// 因此访问不到构造函数本身,也就访问不到实例属性
Person.prototype.say = () => { console.log(this.name) }
- 简洁的语法形式:箭头函数使用了更简洁的语法形式,省略了传统函数声明中的
function
关键字和大括号。它通常可以在更少的代码行数中表达相同的逻辑。 - 没有自己的this:箭头函数没有自己的
this
绑定,它会捕获所在上下文的this
值。这意味着箭头函数中的this
与其定义时所在的上下文中的this
保持一致,而不是在函数被调用时动态绑定。这可以避免传统函数中常见的this
指向问题,简化了对this
的使用和理解。 - 没有
arguments
对象:箭头函数也没有自己的arguments
对象。如果需要访问函数的参数,可以使用剩余参数(Rest Parameters)或使用展开运算符(Spread Operator)将参数传递给其他函数。 - 无法作为构造函数:箭头函数不能用作构造函数,不能使用
new
关键字调用。它们没有prototype
属性,因此无法使用new
关键字创建实例。 - 隐式的返回值:如果箭头函数的函数体只有一条表达式,并且不需要额外的处理逻辑,那么可以省略大括号并且该表达式将隐式作为返回值返回。
- 不能绑定自己的this、super、new.target:由于箭头函数没有自己的
this
绑定,也无法使用super
关键字引用父类的方法,也无法使用new.target
获取构造函数的引用。
作用
- 简化普通函数:箭头函数提供了更简洁的语法形式,可以在需要定义函数的地方使用更短的代码来表达同样的逻辑。这可以提高代码的可读性和维护性。
- 保留上下文:箭头函数没有自己的
this
绑定,它会捕获所在上下文的this
值。这意味着在箭头函数中,this
的值是在函数定义时确定的,而不是在函数被调用时动态绑定。这种特性可以避免传统函数中的this
绑定问题,并使代码更易于理解和维护。
使用场景
- 简化函数表达式:当需要定义一个简单的函数表达式时,可以使用箭头函数代替传统的函数表达式,减少代码量。
// 传统函数表达式
const sum = function (a, b) {
return a + b
}
// 箭头函数
// const sum = (a, b) => a + b;
- 箭头函数作为回调函数:当需要传递回调函数时,箭头函数可以提供更简洁的语法形式,同时保留外层上下文中的
this
。
// 传统回调函数
someFunction(function () {
console.log(this) // 外层上下文的this
})
// 箭头函数作为回调函数
someFunction(() => {
console.log(this) // 外层上下文的this
})
- 简化函数中的
this
绑定问题:由于箭头函数没有自己的this
绑定,可以避免使用传统函数中常见的bind
、call
或apply
等方法来绑定this
。
// 传统函数中的this绑定
const obj = {
value: 42,
getValue: function () {
setTimeout(function () {
console.log(this.value) // undefined,因为此时this指向全局对象
}, 1000)
}
}
// 使用箭头函数避免this绑定问题
const obj1 = {
value: 42,
getValue: function () {
setTimeout(() => {
console.log(this.value) // 42,箭头函数捕获了外层上下文的this
}, 1000)
}
}
// ```
箭头函数是ES6中引入的一种新的函数语法,它主要解决了以下几个问题:
-
简化函数表达式:箭头函数提供了一种更简洁的函数定义方式,可以用更短的语法来定义函数,减少了冗余的代码。例如,使用箭头函数可以将一个函数表达式
function(x) { return xx; }
简化为(x) => xx;
。 -
简化this的指向:在传统的函数定义中,函数内部的
this
指向的是调用该函数的对象。而在箭头函数中,this
的指向是在定义函数时确定的,指向的是箭头函数所在的上下文。这解决了传统函数中this
指向容易混淆的问题,使得代码更加易读和简洁。 -
消除了
arguments
对象:在箭头函数中,不存在arguments
对象,这是因为箭头函数没有自己的arguments
,它继承了所在上下文的arguments
。这样可以避免在传统函数中使用arguments
对象时出现的一些问题,如无法使用arguments
对象的一些方法,以及与命名参数的冲突等。 -
适用于回调函数:箭头函数的简洁性和对
this
指向的处理使其特别适用于作为回调函数使用。在传统的函数定义中,由于this
指向的问题,经常需要使用额外的变量来绑定this
,而箭头函数可以直接使用外层作用域的this
,减少了代码的复杂性。
箭头函数也有一些限制和注意事项,例如箭头函数没有自己的arguments
、super
和new.target
,不能作为构造函数使用。
在箭头函数中,this
指向的是定义时所在的对象,而不是使用时所在的对象。换句话说,箭头函数没有自己的this,而是继承父作用域中的this。
看个例子:
const person = {
name: '张三',
age: 18,
getName: function () {
console.log('我的名字是:' + this.name)
},
getAge: () => {
console.log('我的年龄是:' + this.age)
}
}
person.getName() // 我的名字是张三
person.getAge() // 我的年龄是undefined
person.getName()
中this
指向函数的调用者,也就是person
实例,因此this.name = "张三"
。
getAge()
通过箭头函数定义,而箭头函数是没有自己的this
,会继承父作用域的this
,因此person.getAge()
执行时,此时的作用域指向window
,而window
没有定义age
属性,所有报undefined
。
从例子可以得出:对象中定义的函数使用箭头函数是不合适的。
先解答下标题问题,为啥箭头函数不能作为构造函数?
// 构造函数生成实例的过程
function Person (name, age) {
this.name = name
this.age = age
}
var p = new Person('张三', 18)
// new关键字生成实例过程如下
// 1. 创建空对象p
// eslint-disable-next-line
var p = {}
// 2. 将空对象p的原型链指向构造器Person的原型
// eslint-disable-next-line
p.__proto__ = Person.prototype
// 3. 将Person()函数中的this指向p
// 若此处Person为箭头函数,而没有自己的this,call()函数无法改变箭头函数的指向,也就无法指向p。
Person.call(p)
构造函数是通过new关键字来生成对象实例,生成对象实例的过程也是通过构造函数给实例绑定this的过程,而箭头函数没有自己的this。创建对象过程,new
首先会创建一个空对象,并将这个空对象的__proto__
指向构造函数的prototype
,从而继承原型上的方法,但是箭头函数没有prototype
。因此不能使用箭头作为构造函数,也就不能通过new操作符来调用箭头函数。
普通函数动态参数 和 箭头函数的动态参数有什么区别?
普通函数和箭头函数在处理动态参数方面有以下区别:
- 普通函数的动态参数:
- 在普通函数中,可以使用
arguments
对象来访问传递给函数的所有参数,无论是否定义了具名参数。arguments
是一个类数组对象,可以通过索引访问每个参数的值。 - 普通函数可以使用剩余参数语法(Rest parameters)来声明动态参数,通过三个点(
...
)和一个参数名表示。剩余参数会被收集成一个真正的数组,可以直接使用数组的方法和属性对参数进行操作。
示例:
function sum (a, b, ...rest) {
console.log(a, b) // 输出前两个参数
console.log(rest) // 输出剩余的动态参数,作为数组
}
sum(1, 2, 3, 4, 5) // 输出: 1 2, [3, 4, 5]
- 箭头函数的动态参数:
- 箭头函数不具有自己的
arguments
对象。在箭头函数中,无法直接访问传递给函数的所有参数的类数组对象。 - 箭头函数可以使用剩余参数语法来声明动态参数,与普通函数相同。剩余参数会被收集成一个真正的数组,可以直接使用数组的方法和属性对参数进行操作。
示例:
const sum = (a, b, ...rest) => {
console.log(a, b) // 输出前两个参数
console.log(rest) // 输出剩余的动态参数,作为数组
}
sum(1, 2, 3, 4, 5) // 输出: 1 2, [3, 4, 5]
总结:
- 普通函数和箭头函数都可以接受动态参数。
- 普通函数可以使用
arguments
对象访问所有参数,也可以使用剩余参数语法将参数收集成数组。 - 箭头函数没有自己的
arguments
对象,无法直接访问所有参数,但可以使用剩余参数语法将参数收集成数组。
在 JavaScript 中,函数的 arguments
参数被设计为类数组对象,而不是真正的数组。这是因为 arguments
对象包含了函数调用时传入的所有参数,包括未命名的参数。它提供了一种方便的方式来访问和操作这些参数。
要遍历类数组对象,可以使用以下方法:
- 使用 for 循环和索引:通过使用普通的 for 循环和索引来遍历类数组对象。
function sum () {
for (let i = 0; i < arguments.length; i++) {
console.log(arguments[i])
}
}
sum(1, 2, 3) // 输出:1 2 3
- 使用 for...of 循环:
arguments
是特殊的类数组, 因为他实现了[Symbol.iterator]
迭代器, 故可以使用 for...of 循环
function sum () {
for (const arg of arguments) {
console.log(arg)
}
}
sum(1, 2, 3) // 输出:1 2 3
- 将类数组对象转换为真正的数组后遍历:可以使用上述提到的类数组转换方法将类数组对象转换为真正的数组,然后使用数组的遍历方法进行遍历,如
forEach()
、map()
等。
function sum () {
const args = Array.from(arguments)
args.forEach(arg => {
console.log(arg)
})
}
sum(1, 2, 3) // 输出:1 2 3
这些方法都可以用于遍历类数组对象,根据需求选择适合的方式进行操作。
call,apply,bind 区别?
答案
call、apply 和 bind 都是 JavaScript 中用于改变函数执行上下文(即 this 指向)的方法,它们的区别和用法如下:
all
call 方法可以改变函数的 this 指向,同时还能传递多个参数。 call 方法的语法如下:
fun.call(
thisArg, arg1, arg2 // ...
)
fun:要调用的函数。 thisArg:函数内部 this 指向的对象。 arg1, arg2, ...:传递给函数的参数列表。
call 方法的使用示例:
const person = {
name: 'Alice',
sayHello: function () {
console.log(`Hello, ${this.name}!`)
}
}
const person2 = {
name: 'Bob'
}
person.sayHello.call(person2) // 输出:Hello, Bob!
pply
apply 方法和 call 方法类似,它也可以改变函数的 this 指向,但是它需要传递一个数组作为参数列表。 apply 方法的语法如下:
fun.apply(thisArg, [argsArray])
fun:要调用的函数。 thisArg:函数内部 this 指向的对象。 argsArray:传递给函数的参数列表,以数组形式传递。
apply 方法的使用示例:
const person = {
name: 'Alice',
sayHello: function (greeting) {
console.log(`${greeting}, ${this.name}!`)
}
}
const person2 = {
name: 'Bob'
}
person.sayHello.apply(person2, ['Hi']) // 输出:Hi, Bob!
ind
bind 方法和 call、apply 方法不同,它并不会立即调用函数,而是返回一个新的函数,这个新函数的 this 指向被绑定的对象。 bind 方法的语法如下:
// fun.bind(thisArg[, arg1[, arg2[, ...]]])
fun:要调用的函数。 thisArg:函数内部 this 指向的对象。 arg1, arg2, ...:在调用函数时,绑定到函数参数的值。
bind 方法的使用示例:
const person = {
name: 'Alice',
sayHello: function () {
console.log(`Hello, ${this.name}!`)
}
}
const person2 = {
name: 'Bob'
}
const sayHelloToBob = person.sayHello.bind(person2)
sayHelloToBob() // 输出:Hello, Bob!
参数传递 在使用 bind 方法时,我们可以通过传递参数来预先填充函数的一些参数,这样在调用函数时只需要传递剩余的参数即可。
const person = {
name: 'Alice',
sayHello: function (greeting, punctuation) {
console.log(`${greeting}, ${this.name}, ${punctuation}`)
}
}
const person2 = {
name: 'Bob'
}
const sayHelloToBob = person.sayHello.bind(person2)
sayHelloToBob(1, 2) // 输出:1, Bob, 2
再举一个例子:
this.x = 9 // 在浏览器中,this 指向全局的 "window" 对象
const module = {
x: 81,
getX: function () { return this.x }
}
module.getX() // 81
const retrieveX = module.getX
retrieveX()
// 返回 9 - 因为函数是在全局作用域中调用的
// 创建一个新函数,把 'this' 绑定到 module 对象
// 新手可能会将全局变量 x 与 module 的属性 x 混淆
const boundGetX = retrieveX.bind(module)
boundGetX() // 81
call,apply,bind
是函数对象三个重要的原型方法,用来动态改变 js 函数执行时的 this 的指向
- 相同点
- 都是用来改变 this 的值
- 不同点
- call,apply 传参方式不同,直接返回执行结果
- call 调用格式
function.call(thisArg, arg1, arg2, ...)
- apply 调用格式
func.apply(thisArg, [argsArray])
- call 调用格式
- bind 调用格式为
function.bind(thisArg[, arg1[, arg2[, ...]]])
返回的是函数
- call,apply 传参方式不同,直接返回执行结果
引擎是如何实现对执行环境动态改变的?
IIFE 是什么?
答案
IIFE 是立即执行函数表达式。
要理解 IIFE 必须知晓函数申明和函数表达式的区别。一个最简单的判别方法是若 function 关键字未出现一行的开头则且不属于上一行的组成部分则为函数申明否则为函数表达式。
所以只要函数表达式被立即调用则称之为 IIFE.
此外函数表达式和函数申明具有如下区别
- 函数表达式不会被提升
- 函数表达式的标识符是可省略的
- 函数表达式的标识符作用域为该函数体,外部执行环境无法使用
- 具名函数表达式的名称作为函数名,匿名函数名称取决于申明方式
推荐阅读:
generator
- generator 是迭代器
- 用来实现协程
- 典型使用场景
enerator 基本概念
ES6中的 Generator(生成器)是一种特殊类型的函数,它可以被暂停和恢复。这意味着在调用Generator函数时,它不会立即执行,而是返回一个可暂停执行的Generator对象。在需要的时候,可以通过调用.next()方法来恢复函数的执行。这使得我们能够编写更具表现力和灵活性的代码。
Generator函数使用特殊的语法:在函数关键字后面添加一个星号(*)。Generator函数中可以使用一个新的关键字yield,用于将函数的执行暂停,并且可以将一个值返回给调用者。
以下是一个简单的 Generator 函数的例子:
function * generateSequence () {
yield 1
yield 2
yield 3
}
const generator = generateSequence()
console.log(generator.next().value) // 1
console.log(generator.next().value) // 2
console.log(generator.next().value) // 3
在上面的例子中,generateSequence()是一个Generator函数,它定义了一个简单的数列生成器。在函数中,使用了yield关键字,以便能够暂停函数执行。最后,我们通过调用generator.next()方法来恢复函数的执行,并逐步返回生成器中的每一个值。
Generator函数也可以接收参数,并且可以在每次迭代时使用不同的参数值。这使得它们能够以更灵活的方式生成数据。
以下是一个带参数的Generator函数的例子:
function * generateSequence (start, end) {
for (let i = start; i <= end; i++) {
yield i
}
}
const generator = generateSequence(1, 5)
console.log(generator.next().value) // 1
console.log(generator.next().value) // 2
console.log(generator.next().value) // 3
console.log(generator.next().value) // 4
console.log(generator.next().value) // 5
Generator是一种非常有用的工具,它能够帮助我们编写更灵活和表达力强的代码。它们在异步编程、迭代器和生成器等场景中得到了广泛的应用。
async/await 有啥关系?
Generator 和 async/await 都是 ES6 中引入的异步编程解决方案,它们本质上都是利用了 JavaScript 中的协程(coroutine)。
Generator 和 async/await 都是 JavaScript 中用于异步编程的方式,它们的本质相同,都是利用了生成器函数的特性来实现异步操作。
在 ES5 中,JavaScript 使用回调函数实现异步编程,但是这样会导致回调嵌套过深,代码可读性差、难以维护。Generator 和 async/await 的出现解决了这个问题,它们让异步编程更加像同步编程,使代码可读性和可维护性得到了大幅提升。
Generator 可以使用 yield 语句来暂停函数执行,并返回一个 Generator 对象,通过这个对象可以控制函数的继续执行和结束。而 async/await 则是基于 Promise 实现的语法糖,可以使异步代码看起来像同步代码,代码结构更加清晰明了。
在使用上,Generator 和 async/await 都需要通过一些特定的语法来实现异步操作,不同的是 async/await 通过 await 关键字等待 Promise 对象的解决,而 Generator 则是通过 yield 关键字暂停函数执行,并返回一个 Generator 对象,通过 next 方法控制函数的继续执行和结束。另外,async/await 可以更好地处理 Promise 的错误,而 Generator 需要使用 try/catch 语句来捕获错误。
Generator 和 async/await 可以互相转换,这意味着我们可以使用 Generator 来实现 async/await 的功能,也可以使用 async/await 来调用 Generator 函数。
function * gen () {
yield 1
yield 2
yield 3
}
async function test1 () {
for await (const x of gen()) {
console.log(x)
}
}
test1() // 输出 1, 2, 3
在上面的代码中,for await...of
循环语句可以遍历 Generator
函数生成的迭代器,从而实现异步迭代。注意在调用 for await...of 时需要使用 yield* 关键字来进行委托。
Generator 函数使用 await 调用示例:
function * generator () {
const result1 = yield asyncTask1()
const result2 = yield asyncTask2(result1)
return result2
}
async function runGenerator () {
const gen = generator()
const result1 = await gen.next().value
const result2 = await gen.next(result1).value
const finalResult = await gen.next(result2).value
console.log(finalResult)
}
runGenerator()
在 JavaScript 中,有三种类型的迭代器:
-
Array Iterator(数组迭代器):通过对数组进行迭代以访问其元素。
-
String Iterator(字符串迭代器):通过对字符串进行迭代以访问其字符。
-
Map Iterator(映射迭代器)和 Set Iterator(集合迭代器):通过对 Map 和 Set 数据结构进行迭代以访问其键和值。
此外,在 ES6 中,我们还可以使用自定义迭代器来迭代对象中的元素。我们可以使用 Symbol.iterator 方法来创建自定义迭代器,该方法返回一个具有 next 方法的迭代器对象。
另外,Generator
函数可以看作是一种特殊的迭代器,它能够暂停执行和恢复执行,使得我们可以通过控制迭代器的执行来生成序列。
rray Iterator(数组迭代器)有哪些迭代方法?
Array Iterator(数组迭代器)是针对 JavaScript 数组的迭代器,它可以通过 Array.prototype[Symbol.iterator]()
方法来获取。
获取到数组迭代器后,我们可以使用以下迭代方法:
next()
: 返回一个包含 value 和 done 属性的对象,value 表示下一个元素的值,done 表示是否迭代结束。
return()
: 用于提前终止迭代,并返回给定的值。
throw()
: 用于向迭代器抛出一个异常。
下面是一个使用迭代器的示例代码:
const arr = ['a', 'b', 'c']
const iterator = arr[Symbol.iterator]()
console.log(iterator.next()) // { value: 'a', done: false }
console.log(iterator.next()) // { value: 'b', done: false }
console.log(iterator.next()) // { value: 'c', done: false }
console.log(iterator.next()) // { value: undefined, done: true }
除了以上的迭代方法,还可以通过 for...of 语句来使用迭代器,如下所示:
const arr = ['a', 'b', 'c']
for (const item of arr) {
console.log(item)
}
// output:
// a
// b
// c
另外,数组迭代器除了上述的迭代方法,还可以使用 forEach()、map()、filter()、reduce() 等常见数组方法进行迭代操作;
tring Iterator(字符串迭代器) 有哪些迭代方法?
String Iterator
是 ES6 引入的一种迭代器,可以用于遍历字符串。String Iterator 没有自己的迭代方法,但可以使用通用的迭代方法。以下是 String Iterator 可以使用的迭代方法:
next()
:返回迭代器的下一个值,格式为 {value: string, done: boolean}
。
Symbol.iterator
:返回一个迭代器对象,可以使用 for...of 循环来遍历字符串。
示例代码如下:
const str = 'hello'
const strIterator = str[Symbol.iterator]()
console.log(strIterator.next()) // { value: 'h', done: false }
console.log(strIterator.next()) // { value: 'e', done: false }
console.log(strIterator.next()) // { value: 'l', done: false }
console.log(strIterator.next()) // { value: 'l', done: false }
console.log(strIterator.next()) // { value: 'o', done: false }
console.log(strIterator.next()) // { value: undefined, done: true }
for (const char of str) {
console.log(char)
}
// Output:
// h
// e
// l
// l
// o
ap Iterator(映射迭代器)和 Set Iterator(集合迭代器)有哪些迭代方法?
Map Iterator 和 Set Iterator 都有以下迭代方法:
next()
: 返回迭代器中下一个元素的对象,对象包含 value 和 done 两个属性。value 属性是当前元素的值,done 属性表示迭代器是否已经迭代完成。
Symbol.iterator
: 返回迭代器本身,使其可被 for...of 循环使用。
Map Iterator 还有以下方法:
entries()
: 返回一个新的迭代器对象,该迭代器对象的元素是 [key, value] 数组。
keys()
: 返回一个新的迭代器对象,该迭代器对象的元素是 Map 中的键名。
values()
: 返回一个新的迭代器对象,该迭代器对象的元素是 Map 中的键值。
Set Iterator 还有以下方法:
entries()
: 返回一个新的迭代器对象,该迭代器对象的元素是 [value, value] 数组。
keys()
: 返回一个新的迭代器对象,该迭代器对象的元素是 Set 中的值。
values()
: 返回一个新的迭代器对象,该迭代器对象的元素是 Set 中的值。
Map Iterator 使用举例
const myMap = new Map()
myMap.set('key1', 'value1')
myMap.set('key2', 'value2')
myMap.set('key3', 'value3')
const mapIterator = myMap.entries()
console.log(mapIterator.next().value) // ["key1", "value1"]
console.log(mapIterator.next().value) // ["key2", "value2"]
console.log(mapIterator.next().value) // ["key3", "value3"]
console.log(mapIterator.next().value) // undefined
Set Iterator 使用举例
const mySet = new Set(['apple', 'banana', 'orange'])
// 使用 for...of 循环遍历 Set
for (const item of mySet) {
console.log(item)
}
// 使用 Set 迭代器手动遍历 Set
const setIterator = mySet.values()
let next = setIterator.next()
while (!next.done) {
console.log(next.value)
next = setIterator.next()
}
Generator 是 JavaScript 中一种特殊的函数,它能够通过迭代器协议(Iterator Protocol)实现中断和恢复的功能。
Generator 函数使用 function*
声明,内部可以使用 yield
关键字来定义中断点。当调用 Generator 函数时,它不会立即执行,而是返回一个迭代器对象。通过调用迭代器的 next()
方法,可以逐步执行 Generator 函数,并在每个 yield
关键字处暂停执行并返回一个包含当前值的对象。
当调用 next()
方法时,Generator 函数会从上次暂停的地方继续执行,直到遇到下一个 yield
关键字或函数结束。通过不断调用 next()
方法,可以逐步执行 Generator 函数的代码,并获取每个中断点处的值。
由于 Generator 函数具有中断和恢复的特性,可以用于异步编程,实现一种更直观的方式来处理异步操作。通过 yield
关键字,可以将异步操作分割成多个步骤,每个步骤都可以通过 yield
暂停,等待异步操作完成后再恢复执行。
以下是一个简单的示例,展示了 Generator 函数的中断和恢复特性:
function * generatorFunction () {
console.log('Step 1')
yield
console.log('Step 2')
yield
console.log('Step 3')
}
const generator = generatorFunction()
generator.next() // Step 1
generator.next() // Step 2
generator.next() // Step 3
在上述示例中,我们定义了一个名为 generatorFunction
的 Generator 函数。在函数体内,使用 console.log
打印了三个不同的步骤,并在每个步骤后使用 yield
关键字暂停执行。然后,我们通过调用 generator.next()
方法逐步执行 Generator 函数。每次调用 next()
方法时,函数会从上次暂停的地方恢复执行,打印相应的步骤。
通过使用 Generator 函数,可以实现更灵活、可控的异步编程模式,提供更好的代码可读性和维护性。