函数用于封装一段完成特定功能的代码,相当于将包含一条或多条语句的代码块“包裹”起来,用户在使用时只需关心参数和返回值,就能完成特定的功能。
函数的优势在于提高代码的复用性,降低程序维护的难度。
6.1函数的定义与调用
自定义函数的语法格式如下。
function 函数名([参数 1, 参数 2, …]) {
函数体
}
函数的定义以下 4 部分组成
- function:定义函数的关键字。
- 函数名:可由字母、数字、下画线和$符号组成,不能以数字开头,不能是
JavaScript 中的关键字。
3.参数:外界传递给函数的值,此时为形参,可选的,多个参数之间使用“,”分隔。
4.函数体:由函数内所有代码组成的整体,专门用于实现特定功能。函数体内使用return 关键 字可以返回函数的结果。
通过“函数名()”的方式即可实现函数的调用,小括号中可以传入参数,其语法格式如下。
函数名称([参数 1, 参数 2, …])
参数表示实参,“[参数 1,参数 2…]”表示实参列表,实参个数可以是零个、一个或多个。
通常情况下,函数的实参列表与形参列表顺序对应,当函数体内不需要参数时,调用时可以不传参。
函数定义与调用的编写顺序不分前后
6.1.1 this关键字
this 是一个关键字,能够在函数执行过程中访问运行环境,它的值根据函数的调用方式和上下文而变化,所以 this 是动态的,动态指向当前函数的运行环境。
this 是一个对象,当函数执行时产生的内部对象,this 指向只与函数的执行环节有关,与函数的声明无关。
this 是一个指针型变量,在 JavaScript 中没有指针的概念,但是this真实地指向当前调用对象,能够在函数内部访问和操作当前对象“的属性和方法。
6.1.2函数作为方法被调用
当一个函数被赋值给一个对象的属性,并且通过对象属性引用的方式调用函数时,函数会作为对象的方法被调用。
var person = {
Name: "liux",
Name2: "Dog",
fullName: function() { //函数
return this.Name + " " + this.Name2;
}
};
console.log(person.fullName()); //fullName 是 person 对象的一个方法。
6.1.3使用构造函数调用函数
一般来讲,使用关键字 new 调用函数时会触发以下几个动作:
(1)创建一个新对象
(2)该对象作为 this 参数传递给构造函数,从而成为构造函数的函数上下文。
构造函数的目的是创建一个新对象,并进行初始化设置,然后将其作为构造函数的返回值。
案例:
function a1(name) { // 定义一个普通函数
console.log("你好, " + name);
}
function a2(name) { // 定义一个构造函数,并在其中调用上面定义的函数
a1(name); // 在这里调用a1函数
this.name = name; // 初始化对象的 name 属性
this.greet = function() { //this.greet()是一个方法
console.log("你好,我是" + this.name);
};
}
var a3 = new a2("lx"); // 使用构造函数创建一个a2的实例,即对象a3
a3.greet(); // 调用a3对象的方法(a3 对象获得了 greet 方法)
6.1.4作为函数方法调用函数
到目前为止,不同类型函数调用之间的主要区别在于:最终作为函数上下文(this)传递给执行函数的对象不同。那如果我想要显示地指定一个函数在调用时的上下文怎么办?这时候就要用到call() 和 apply()了。
call() 和 apply()是存在于每个函数上的。
call() 接收第二个参数为参数列表,参数列表中的元素作为参数传递给函数
apply() 接收第二个参数为数组,数组中的元素作为参数传递给函数
与 call() 不同的是,apply() 第二个参数必须是一个数组,数组中的元素会被当作参数列表传递给函数
function add(a, b) {
return a + b;
}
//使用call()
result1 = ' '; //定义对象
result1 = add.call(result1, 2, 3); // 2 + 3 = 5
alert(result1); // 5
使用apply()
Array=[2,3];
result2 = ' ';
result2 = add.apply(result2, Array); // 2 + 3 = 5
alert(result2);
6.2函数参数设置
6.2.1有参函数,无参函数
函数在定义时根据参数的不同,可分为两种类型。
- 无参函数:定义函数时不设置参数的函数。
- 有参函数:定义函数时设置了参数的函数。
//无参函数
function A() {
console.log("Hello World!");
}
A();
//有参函数
function B(name) {
console.log("Hello " + name + "!");
}
B("John");
补充:
(1)含有默认值的参数
在设置函数的形参时,还可以为其指定默认值。当调用者未传递该参数时,函数将使用默认值进行操作。
// 含有默认值的参数
function C(name = "World") {
console.log("Hello " + name + "!");
}
C();
C("John");
(2)剩余参数
在函数定义时,除了可以指定具体数量的形参外,还可以利用“…变量名”的方式动态的接收用户传递的不确定数量的实参
// 剩余参数 参数的数量不确定
function D(...args) {
console.log(args);
}
D(1, 2, 3);
6.2.2函数内外变量的作用域
在JavaScript中,变量的作用域由其定义的位置确定。变量可以分为全局变量和局部变量。
(1)全局变量:在函数外定义的变量是全局变量,它可以在函数内部访问,也可以在函数外部访问。
var globalVar = 'I am a global variable';
function checkScope() {
console.log(globalVar); // 输出 'I am a global variable'
}
console.log(globalVar); // 输出 'I am a global variable'
(2)局部变量:在函数内部使用var关键字定义的变量是局部变量,它只能在该函数内部访问。
function localScope() {
var localVar = 'I am a local variable';
console.log(localVar); // 输出 'I am a local variable'
}
localScope();
// console.log(localVar); // 报错,因为localVar是局部变量,不能在此访问
(3)没有使用var定义的变量也是局部变量,即使它在函数内部定义。
function checkScope() {
myVar = 'I am a local variable without var';
console.log(myVar); // 输出 'I am a local variable without var'
}
checkScope();
// console.log(myVar); // 报错,因为myVar是局部变量,不能在此访问
(4)函数的参数也是局部变量,它们只在函数内部有效。
function checkParam(param) {
console.log(param); // 输出 'I am a parameter'
// console.log(arguments[0]); // 输出 'I am a parameter',arguments对象可以访问所有参数
}
checkParam('I am a parameter');
(5)函数内部可以访问全局变量,但函数外部不能访问函数内的局部变量。
var globalVar = 'I am a global variable';
function checkScope() {
var localVar = 'I am a local variable';
console.log(globalVar); // 输出 'I am a global variable'
// console.log(localVar); // 输出 'I am a local variable'
}
}
checkScope();
console.log(localVar); // 报错,因为localVar是局部变量,不能在此访问
(6)函数内部可以通过this关键字访问全局对象,因此可以添加全局变量。
this.globalVar = 'I am a global variable';
function checkScope() {
console.log(this.globalVar); // 输出 'I am a global variable'
}
checkScope();
6.3进阶函数
6.3.1 函数表达式
(1)函数表达式是将声明的函数赋值给一个变量,通过“变量名()”的方式即可完函数的调用,小括号“()”内可以传入参数,函数表达式的定义必须在调用前。
示例代码如下。
var fn = function sum(num1, num2) { // 定义求和函数表达式
return num1 + num2;
};
fn(2, 3); // 调用函数
(2)函数表达式与函数的区别
a. 函数表达式的定义必须在调用前,且函数调用时采用的是“变量名()”的方式。
b. 函数定义的方式不限制定义与调用的顺序。
c. 函数表达式中的函数名如果不需要可以省略。
6.3.2 匿名函数
(1)为什么要用匿名函数
团队合作完成项目时,程序员经常定义一些函数来实现特定的功能,在给这些函数命名时经常会遇到与其他人取相同名字的情况,如何来解决命名冲突问题呢?
使用 JavaScript 中的匿名函数可以有效避免函数名的冲突问题。
所谓匿名函数指的是没有名字的函数,也就是在定义函数时省略函数名。
(2)匿名函数的实现方式
匿名函数可以通过函数定义的方式实现调用,下面介绍匿名函数的 3 个使用场景。
a. 函数表达式中省略函数名
b. 匿名函数自调用
c. 处理事件
a.函数表达式中省略函数名
利用函数表达式实现匿名函数,调用时使用“变量名()”,示例代码如下。
var fn = function (num1, num2) {
return num1 + num2;
};
通常情况下,如果函数的返回值需要使用变量来接收时,可以使用函数表达式来实现匿名函数的调用,并且可以通过“变量名()”的方式调用多次。
b. 匿名函数自调用
使用小括号“()”直接包裹匿名函数,示例代码如下。
(function (num1, num2) {
console.log(num1 + num2);
})(2, 3);
匿名函数后小括号“()”表示给匿名函数传递参数并立即执行,完成函数的自调用。自调用只能调用一次。
c. 处理事件
使用匿名函数处理单击事件,示例代码如下。
document.body.onclick = function () {
alert('Hi, everybody!'); }
6.3.3箭头函数
(1)箭头函数在JavaScript中可以被视为一种匿名函数的形式。这是因为箭头函数没有传统的函数名(即使用function关键字后紧跟的函数名),而是直接通过箭头(=>)来定义函数体。
(2)用途:箭头函数通常用于需要函数但不需要显式函数名的场景,比如作为回调函数传递给其他函数,或者在需要匿名函数表达式的地方。它们提供了一种更简洁的语法来编写函数,并且还有一些与传统函数不同的行为特性,比如不绑定自己的this、arguments、super或new.target
(3)箭头函数是 ES6 中新增的函数,它用于简化函数定义的语法,其语法格式如下。
() => { };
箭头函数的小括号中可以传入参数,调用箭头函数时可以将箭头函数赋给一个变量,然后 通过变量名实现箭头函数的调用。
var fn = (num1, num2) => {
return num1 + num2;
};
箭头函数存在以下两种特殊情况:
(1)省略大括号和 return 关键字
在箭头函数中,当函数体只有一句代码,且代码的执行结果就是函数的返回值时,可以省略函数体的大括号以及 return 关键字。
(2)省略参数外部小括号
在箭头函数中,当参数只有 1 个时,可以省略参数外部的小括号。
6.3.4回调函数
回调可用于数组、计时器函数、 promise、事件处理中。两种回调:同步和异步。
1.回调函数
(1) 回调函数指的就是一个函数A作为参数传递给一个函数B,然后在B的函数体内调用函数A。此时,称函数A为回调函数。
(2) 可以这样理解:比如我们有3个函数A,B,C,一个主函数里面有一个功能函数,功能函数需要用到A时就会调用A,当它需要用B时就要删掉A再调用B,这样就会很麻烦。因此我们将函数A,B,C当做参数,在每次功能函数需要时以参数形式进行传递,功能函数可以通过修改参数来调用不同的函数,而其本身不用做修改。我们称A,B,C为回调函数
案例:
function a1() { // 定义三个不同的处理函数
console.log('执行了函数 1');
}
function a2() {
console.log('执行了函数 2');
}
function a3() {
console.log('执行了函数 3');
}
function A(operation) { // 功能函数接受一个回调函数作为参数
operation(); // 调用传入的回调函数
console.log('执行完毕');
}
function main() { // 主函数
A(a1); // 使用函数1作为回调
A(a2); // 使用函数2作为回调
A(a3); // 使用函数3作为回调
}
main(); // 调用主函数
2.高阶函数:
则是用另一个函数作为参数的函数,或者返回一个函数的函数
回调函数作为高阶函数的参数,高阶函数通过调用回调函数来执行操作。重要的是高阶函数负责调用回调,并为其提供正确的参数。
案例:
function A1(a, callback) { //callback 回调函数
var result = a * 2;
if (typeof callback === 'function') { // 调用回调函数并传递结果
callback(result);
}
}
function A2(result) { // 定义几个回调函数
console.log('结果为'+result);
}
function A3(result) {
var doubled = result * 2;
console.log('2倍结果为'+doubled);
}
// 使用高阶函数
console.log('原始结果:');
A1(5, A2); // 输出: 结果是: 10
console.log('双倍的结果:');
A1(5, A3); // 输出: 双倍的结果是: 20
3.同步回调
同步回调是“阻塞”的:同步回调意味着回调函数在调用它的函数完成其任务后立即执行。在同步回调中,程序的执行顺序是线性的,即按照代码从上到下的顺序执行。
案例:
function A1(a, b, callback) {// 高阶函数,接受两个数字和一个回调函数
var result = a + b;
if (typeof callback === 'function') { // 调用回调函数并传递结果 typeof 可以用来检查各种数据类型的值,包括原始数据类型和引用类型
callback(result);
}
}
function A2(result) { // 定义一个简单的回调函数
console.log('计算结果是: ' + result);
}
console.log('开始计算...'); // 使用高阶函数a1并传递回调函数 `A2`
A1(3, 4, A2); // 传递 3 和 4 以及 A2 回调函数
console.log('计算完成');
*注;A1是一个高阶函数,因为它接受一个回调函数 callback
作为参数,并在内部立即调用该回调函数。由于这是一个同步操作,程序的执行顺序是线性的,即按照代码从上到下的顺序执行。
4. 异步回调函数
异步回调是“非阻塞的”:异步回调意味着回调函数在调用它的函数完成其任务之后某个时间点执行,而不是立即执行。(可确保稍后在特定事件上执行回调)
案例:
function add(a, b, callback) { // 高阶函数,接受两个数字和一个回调函数
setTimeout(function() { //设置定时器,用于设置延迟执行代码的函数
callback(a + b);
}, 1000);
};//表示在 1 秒后,匿名函数 function() 被调用。
function aResult(result) { //Result 是一个函数,接受一个参数 result。
console.log("结果是:" + result);
}
add(2, 3, aResult); //在调用 add 函数,并传入了以下三个参数
5.总结
回调是一个可以作为参数传给另一个函数(高阶函数)执行的函数。
回调函数有两种:同步和异步。
同步回调是阻塞的。
异步回调是非阻塞的。
6.3.5异步回调函数与异步函数
async function 关键字可用于定义表达式中的异步函数(在函数定义之前加上特殊关键字 async 会创建一个异步函数)。
异步回调函数:使用回调函数处理异步操作,适合简单的情况。注意代码可能会变得比较复杂,尤其是有多个异步操作时。
异步函数:使用 async 和 await 让异步代码更易读,整体结构更像同步代码,适合处理复杂的异步流程。
1.异步回调函数
异步回调函数是一种编程模式,其中一个函数在完成某项操作时会调用另一个函数(回调函数)。这个过程是在不阻塞主程序的情况下进行的。
如果上面的案例没懂。看下这个。
案例:
可以想象成在蜜雪冰城点奶茶。
你告诉服务员你的订单,然后服务员去准备你的奶茶。在等待过程中,你可以做其他事情。
2.异步函数
异步函数使用 async 和 await 关键字来简化异步操作的执行。它们让异步代码看起来更像同步代码,从而提高可读性和可维护性。
特点:
更易读:使用 await 可以在调用异步操作时“暂停”代码执行,让异步操作呈现出线性结构。
案例:
function orderFood(callback) {
console.log("点餐中...");
setTimeout(function () { // 模拟准备奶茶的异步操作
callback("奶茶来啦!"); // 准备好后调用回调,传递结果
}, 2000);
}
function eat(food) {
console.log(food); // 输出“食物来啦!”
}
orderFood(eat); // 输出“点餐中...”,然后2秒后输出“奶茶来啦!”
继续用带你点奶茶的案例,不过现在你是使用了异步函数的顾客,你可以先点餐,然后不需要等待准备好后再继续做别的事情。你等餐的同时,可以先去干别的事情。
function orderFood() {
return new Promise((resolve) => {
console.log("点餐中...");
setTimeout(function() {
resolve("奶茶来啦!"); // 在准备好时解决 promise
}, 5000);
});
}
async function eat() {
naicha = await orderFood(); // 等待奶茶的 Promise
console.log(naicha); // 输出“奶茶来啦!”
}
eat(); // 输出“点餐中...”,然后5秒后输出“奶茶来啦!”
其中:Promise 是表示一个异步操作的最终完成(或失败)及其结果值的对象
6.4作用域
6.4.1什么是作用域
作用域是根据名称查找变量的一套规则。
作用域 表示一个区间,在这个区间内声明的所有内容(比如方法或变量)都可以被该区间内的代码访问到。
注意:
- 作用域(Scope) 指一个范围、区域或空间
- 全局作用域(Global Scope) 指全局空间或一个公共空间
在最外层函数和最外层函数外面定义的变量拥有全局作用域 ,变量在函数外定义,即为全局变量。
全局变量可以在脚本和函数内均可访问
全局变量有 全局作用域: 网页中所有脚本和函数均可使用。
- 局部作用域(Local Scope) 指一个局部空间或一个受限制的空间
变量在函数内声明,变量为局部变量。
函数的局部作用域是指在函数内部声明的变量或函数,其生命周期仅限于该函数的执行期间。当函数被调用时,这些局部变量的作用范围只存在于函数内部,一旦函数执行结束,这些变量就会被自动销毁,外部无法直接访问。
举个例子:
// 定义一个全局变量:
const fullName = "Oluwatobi Sofela";
// 定义多层嵌套函数:
function profile() {
function sayName() {
function writeName() {
return fullName;
}
return writeName();
}
return sayName();
}
// 打印结果:
console.log(profile()) // 'Oluwatobi Sofela'
在上述示例中,我们定义了一个fullName
全局变量,这就意味着在脚本内所有代码都可以访问fullName
变量。
我们在sayName()
函数内定义了writeName()
函数,所以writeName()
被sayName()
的局部作用域包裹着。
换言之,writeName()
只能被sayName()
函数内部的代码访问。
请记住,无论writeName()
函数何时被调用,编译器都不会直接访问全局作用域下的fullName
变量,而是通过作用域链依次查找。
只有函数的{}构成作用域,对象的{}以及 if(){}都不构成作用域;
let
声明的变量会产生块作用域,var
不会产生块作用域
const
声明的常量也会产生块作用域
不同代码块之间的变量无法互相访问
6.4.2词法作用域
词法(Lexical) 指的是定义某个事物。
任何创建文字、表达式或变量的声明都叫词法。
词法作用域(Lexical Scope) 是定义表达式并能被访问的区间。
换言之,一个声明(定义变量、函数等)的词法作用域就是它被定义时所在的作用域。
注意:
- 词法作用域又叫静态作用域。
- 一个声明 被调用时的作用域 不一定是它的词法作用域。相反的,定义时的作用域 才是词法作用域
一个词法作用域的小示例如下:
// 定义一个全局作用域变量:
const myName = "Oluwatobi";
// 在函数体内调用myName变量
function getName() {
return myName;
}
console.log(getName()) // 'Oluwatobi'
在上述示例中,我们在全局作用域定义了myName变量,并在getName()函数作用域内调用了该变量。
问题: myName变量的词法作用域是什么?全局作用域还是 getName()的局部作用域?
答案: 切记 词法作用域 意味着 定义时的作用域,并不是调用时的作用域 。因此myName变量的词法作用域是全局作用域,因为我们在全局环境下定义了myName变量。
6.4.3变量提升:
变量提升(Hoisting)是指在JavaScript中的一种语言特性,它指的是变量声明会被提前到其所在作用域的顶部,无论它们的实际声明位置在哪里。这意味着你可以在声明之前就使用变量,JavaScript引擎会查找这个变量并将其赋值为undefined
,直到它的实际初始化。然而,只有变量声明会被提升,而不是变量赋值
6.4.4块级作用域
块级作用域是在代码块(通常是由花括号{}包裹起来的部分)内声明的作用域。比如if(){….}、for(…){…}、try{…}
- JavaScript ES6 引入了块级作用域,通过let和const关键字声明变量,它们只在声明所在的代码块内可见,不能在函数外部被访问
- 块级作用域在循环内部声明变量时特别有用;
- let关键字
- 声明块级作用域变量
使用let关键字声明的变量只在let命令所在的代码块内有效,即let所在的{}内;
案例1:
<script>
function fn(){
let count=99;
console.log("count值为:", count) //输出:count值为:99
{
let count = 88;
console.log("count值为:", count); //输出:count值为:88
}
console.log("count值为:", count); } //输出:count值为:99
fn()
</script>
案例2:
function foo() {
var x = 1;
let y = 2;
if (true) {
var x = 3;
let y = 4;
console.log(x); // 3
console.log(y); // 4
}
console.log(x); // 3
console.log(y); // 2
}
foo();
在这个示例中,var声明的变量x没有块级作用域,因此在if语句块内的赋值会影响到外部的x。而let声明的变量y具有块级作用域,因此在if语句块内的赋值不会影响到外部的y。
- 不存在变量提升
使用let关键字声明的变量不存在变量提升,只能在声明之后被访问
如果在声明之前访问变量,将会抛出一个引用错误
<script>
console.log("使用let声明之前,count值为:", count);
// 报错:ReferenceError: Cannot access 'count' before initialization
let count = 99;
console.log("使用let声明之后,count值为:", count);
</script>
- 声明的变量,可以被重新赋值
使用let关键字声明的变量,可以被重新赋值;
<script>
let count=99;
console.log("使用let声明变量count,值为:", count);
count=88;
console.log("修改后的count,值为:", count);
</script>
- 声明的变量,不能被重新声明
使用let关键字声明的变量,不能被重新声明
<script>
let count=99;
console.log("使用let声明变量count,值为:", count);
let count = 88 ;
//报错: SyntaxError: Identifier 'count' has already been declared
</script>
- 一次可以声明多个变量
使用let关键字可以声明多个变量,中间用逗号隔开
<script>
let price="10.00",count=99;
console.log("let声明变量price,值为:", price);
console.log("let声明变量count,值为",count);
</script>
Const关键字
a.声明块级作用域变量
Const关键字的特点与let相似,主要区别于const关键字用来声明只读的变量,即常量,不能被重新赋值使用
<script>
function fn() {
const product = "苹果";
console.log("product值为:", product);
{
const product = "西瓜";
console.log("product值为:", product);
}
console.log("product值为:", product);
}
fn();
</script>
b.不存在变量提升
使用const关键字声明的变量不存在变量提升,只能在声明之后被访问;
如果在声明之前访问变量,将会抛出一个引用错误;
<script>
console.log("使用const声明之前,product值为:", product);
// 报错:ReferenceError: Cannot access 'product' before initialization
const product = "苹果";
console.log("使用const声明之后,product值为:", product);
</script>
c. 声明的变量,不能被重新赋值
该变量是只读的,即常量;
const product = "苹果";
console.log("使用const声明变量product,值为:", product);
product = "西瓜";
// 报错:TypeError: Assignment to constant variable.
d.声明的变量,不能被重新声明
使用const关键字声明的变量,不能被重新声明;
<script>
const product = "苹果";
console.log("使用const声明变量product,值为:", product);
const product = "西瓜";
// 报错:SyntaxError: Identifier 'product' has already been declared
</script>
e.一次可以声明多个变量
使用const关键字一次可以声明多个变量,中间用逗号隔开即可;
<script>
const price = "9.99", count = 99, product = "苹果";
console.log("使用const声明变量price,值为:", price);
console.log("使用const声明变量count,值为:", count);
console.log("使用const声明变量product,值为:", product);
</script>
6.4.5 作用域链
JavaScript 的作用域链规定了编译器在查找 被调用变量 的词法作用域时所遵循的查找规则。
作用域链是一个独特空间。当一个变量被调用,那么变量在 被调用 时所在的局部作用域和全局作用域之间,就形成了一个作用域链。
示例代码如下:
// 定义一个全局作用域变量:
const fullName = "Oluwatobi Sofela";
// 定义多层嵌套函数:
function profile() {
function sayName() {
function writeName() {
return fullName;
}
return writeName();
}
return sayName();
}
console.log(profile()) // 'Oluwatobi Sofela'
在上述示例中,fullName 变量在 writeName() 函数作用域中被调用。
因此,从变量的执行作用域到全局作用域之间存在如下作用域链:
writeName() scope ---> sayName() scope ---> profile() scope ---> global scope
换言之,从fullName变量的执行作用域到它的词法作用域(此处指全局作用域)之间有4层作用域。
注意: 在 JavaScript 作用域链中,全局作用域是整个作用域链的终点。
6.4.6 递归函数
递归调用是函数嵌套调用中一种特殊的调用,它指的是一个函数在其函数体内调用自身的过程,这种函数称为递归函数。
死递归:没有递归结束的条件就称为死递归
== 递归会消耗大量内存,在实际开发中很少使用==
注意事项:1.一定要有递归结束的条件。2.改变递归条件的代码
案例1:递归函数求1-100的累加值
function sum(){
if(n == 1){
return 1;
}
return sum(n-1) + n
}
sum(100);
6.5闭包函数
1. 作用域和词法作用域,作用域就是查找变量(去哪儿找,怎么找)的一套规则。词法作用域在你写代码的时候就确定了。JavaScript 是基于词法作用域的语言,通过变量定义的位置就能知道变量的作用域。
2. 作用域链:当某个函数第一次被调用时,会创建一个执行环境及相应的作用域链,并把作用域链赋值给一个特殊的内部属性 [[Scope]] 。然后,使用 this 、
arguments 和其他命名参数的值来初始化函数的活动对象。但在作用域中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,...直至作用作用域链终点的全局执行环境。
6.5.1什么是闭包
闭包函数:声明在一个函数中的函数,叫做闭包函数。
闭包:内部函数总是可以访问其所在的外部函数中声明的参数和变量,即使在其外部函数被返回(寿命终结)了之后。
特点:
- 让外部访问函数内部变量成为可能;
- 局部变量会常驻在内存中;
- 可以避免使用全局变量,防止全局变量污染;
- 会造成内存泄漏(有一块内存空间被长期占用,而不被释放)
闭包的创建:
闭包就是可以创建一个独立的环境,每个闭包里面的环境都是独立的,互不干扰。闭包会发生内存泄漏,每次外部函数执行的时候,外部函数的引用地址不同,都会重新创建一个新的地址。但凡是当前活动对象中有被内部子集引用的数据,那么这个时候,这个数据不删除,保留一根指针给内部活动对象。
闭包内存泄漏为: key = value,key 被删除了 value 常驻内存中; 局部变量闭包升级版(中间引用的变量) => 自由变量;
示例代码如下:
<script>
function 外部函数() {
var 局部变量 = "我是一个局部变量";
function 内部函数() {
// 这里就形成了一个闭包
console.log(局部变量);
// 即使外部函数执行完毕,局部变量仍然可访问
}
return 内部函数;
}
var 闭包 = 外部函数();
闭包(); // 输出: 我是一个局部变量
</script>
结论:闭包找到的是同一地址中父级函数中对应变量最终的值
案例练习:
模拟私有变量
JavaScript中没有私有变量的概念,但通过闭包可以模拟出类似私有变量的效果。
<script>
function createCounter(){
let count=0
return{
increment:function(){
count++
return count
},
decrement:function(){
count--
return count
}
}
}
const counter=createCounter()
console.log(counter.increment())
console.log(counter.increment())
console.log(counter.decrement())
</script>
在JavaScript中,冒号(:)在对象字面量(Object Literal)的上下文中被用来分隔对象的属性名(key)和属性值(value)。这是一种定义对象属性和方法的标准语法。
当你看到一个对象字面量中的冒号时,它左边的部分是一个字符串(或符号,但在大多数情况下是字符串),表示属性的名称;而右边的部分则是该属性的值,这个值可以是任何JavaScript数据类型,包括数字、字符串、布尔值、数组、对象(包括函数对象,即方法)等
6.5.2 闭包函数的实现
闭包函数常见的实现方式是在原函数内创建另一个函数,创建函数作为原函数的返回值返回。
闭包是将函数内部和函数外部连接起来的桥梁。
应用场景:
模块化和封装:闭包可以帮助您隐藏内部实现细节,从而创建模块化和封装良好的代码。
计时器和回调:闭包常用于实现定时器(如setTimeout和setInterval)或事件监听器的回调函数。
高阶函数:高阶函数是以其他函数为参数或返回其他函数的函数。闭包使得高阶函数能够访问并操作外部作用域的变量。
常见的一些闭包:
function a(){
var i=0;
function b(){
alert(++i);
}
return b;
}
var c=a();
c();
这段代码有两个特点:
- 函数b嵌套在函数a内部;
- 函数a返回函数b。
这样在执行完var c=a( )后,变量c实际上是指向了函数b,再执行c( )后就会弹出一个窗口显示i的值(第一次为1)。这段代码其实就创建了一个闭包,这是因为函数a外的变量c引用了函数a内的函数b。也就是说,当函数a的内部函数b被函数a外的一个变量引用的时候,就创建了一个闭包。因为函数a被b引用,b又被a外的c引用,这就是为什么函数a执行后不会被回收的原因。
|
|
模块化封装示例:
function createModule(){
let privateVarible='i am private'
function privateFunction(){
return privateVarible
}
function publicFunction(){
return privateFunction()
}
return{
publicFunction:publicFunction
}
}
const myModule=createModule()
console.log(myModule.publicFunction())
</script>
(2) 计时器和回调示例:
unction delayedGreeting(name, delay) {
function greet() {
console.log(`Hello, ${name}!`);
}
setTimeout(greet, delay);
}
delayedGreeting("Amlr", 1000); // 在1秒后输出:"Hello, Amlr!"
delayedGreeting("John", 5000); // 在2秒后输出:"Hello, John!"
(3)计数例
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>计数器</title>
</head>
<body>
<p>局部变量计数。</p>
<button type="button" onclick="myFunction()">计数!</button>
<p id="demo">0</p>
<script>
var add = (function () {
var counter = 0;
return function () {return counter += 1;}
})();
function myFunction(){
document.getElementById("demo").innerHTML = add();
}
</script>
</body>
</html>
(4)高阶函数示例:
function multiplier(factor) {
function multiply(number) {
return number * factor;
}
return multiply;
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(4)); // 输出:8
console.log(triple(4)); // 输出:12
闭包的缺点:
闭包会保留他们包含函数的作用域,更加占用内存。闭包执行完后不能自动销毁,可能会导致内存泄漏,所以在使用完变量后手动为它赋值null会解除对函数的引用。
6.5.3关于this的情况:
(1 )作为对象方法的调用
function f() {
console.log( this.code );
}
let obj = {
code: 200,
f: f
};
obj.f(); // 200
(2)纯粹的函数调用
function f() {
console.log( this.code );
}
// 此处,通过var(函数作用域)声明的变量code会绑定到window上;如果使用let(块作用域)声明变量code,则不会绑定到window上,因此下面的2次函数调用f(),会输出undefined
// let code = 200;
var code = 200;
f(); // 200
code = 404;
f(); // 404
(3)复杂一点
function doF(fn) {
this.code = 404;
fn();
}
function f() {
console.log(this.code);
}
let obj = {
code: 200,
f: f
};
var code = 500;
doF(obj.f); // 404
该列子中,为分析出this的指向,应找到关键点,哪个对象调用了函数f()。obj.f作为doF()的实参,将函数f传给了doF,而doF是由window对象调用的,所以函数doF中的this指向window,继而函数f中的this也指向window。
由于最终执行函数f时,其中的this指向window,所以在函数f中执行this.code = 401时,等同于window.code = 401:
标签:function,console,函数,作用域,JavaScript,变量,log From: https://blog.csdn.net/2401_86036532/article/details/143355252