函数声明
规则:必须指定一个函数名字
foo();
function foo() {
console.log("函数声明")
}
由于函数声明会被提升,所以调用函数可以在之前或之后调用
函数表达式
规则:将函数赋值给一个变量
var foo = function () {
console.log("函数表达式")
}
foo()
由于函数表达式,只会对变量foo进行提升,只有运行到代码处,才会对变量foo进行赋值,所以调用函数必须在函数表达式之后
变量提前声明
变量提前声明,但不赋值
var a = 100;
function test(){
console.log(a); //100
a = 10; //a为全局变量,但已经声明了,不需要重复声明
console.log(a); //10
}
test();
console.log(a); //10
变量提前声明,但不赋值
var a = 100;
function test(){
console.log(a); //undefined
var a = 10; //a为函数内变量需提前声明,var a = undefined,执行到这一行才给a赋值为10
console.log(a); //10
}
test();
console.log(a); //100
立即执行函数
常用的两种方式
(function () {
console.log("立即执行函数")
})();
(function () {
console.log("立即执行函数")
}())
立即执行函数的演变
1:在函数表达式后加括号,可以立即执行函数
var foo = function () {
console.log("立即执行函数")
}();
2:在匿名函数后加括号,不可以立即执行函数
function () {
console.log("立即执行函数")
}();
在函数表达式后加括号可以立即执行函数,但如果不把匿名函数赋值给变量,而直接在后边加一个括号,则会报语法错误。因为当JS引擎在遇到function时,会默认把它当作是函数声明,所以必须要有一个函数名
3:在函数声明后加括号,不可以立即执行函数
function foo() {
console.log("立即执行函数")
}()
如果给匿名函数添加一个函数名,也就变成了函数声明,然后在函数声明后加括号,也会报语法错误,因为函数声明会被提升,当提升后就相当于在代码中只写了一对括号,而这个括号和前面的函数声明又没什么关系,所以语法报错
javascript中,括号内不允许包含语句,但可以是表达式
引擎先遇到括号,然后遇到关键字function , 就自动把括号里面的代码识别为函数表达式,而不是函数声明,所以立即执行函数可以这样写(function(){})()
闭包
定义:内部函数引用外部函数的变量,并且内部函数被外部函数以外的变量引用时,就形成了闭包
注意:内部函数引用外部函数内的变量以及形式参数都会形成闭包。解决办法是当不使用时及时把外部变量置为null
常用方式示例:
function foo() {
var num = 10;
return function () {
var age= 20;
console.log(++num);
console.log(++age);
}
}
var f = foo();
f(); //11 21
f(); //12 21
通过闭包就可以让外部作用域访问函数内部作用域的变量,我们知道当函数执行完以后,会立即销毁,但 foo 函数内的 num 属性被内部的匿名函数引用着,而内部函数又被外部变量 f 引用着,所以函数 foo 在执行完以后虽然会立即销毁,但它内部的匿名函数在创建的时候就会随之创建一个特殊的容器,用于保存 上层作用域 中 变量 的引用,所以foo 函数中的 num 并不会销毁,当执行第一次 f() 时,会创建 f() 对应的作用域,其中num 会从之前创建的特殊的容器中取出上层作用域中变量的值,而 age 会立即声明一个,当函数执行完毕后,就会销毁对应的作用域,此时 age 也会随之销毁,但变量 f 对应的指针地址不会变,当再次执行 f() 时,又会再创建一个 f() 对应的作用域,num 还是会从上层作用域中拿,但 age 还是会重新声明,使用完后还是会被销毁。
很显然这样会一直持有对 num 的引用,无法进行回收,造成内存占用,所以当不使用时,可以把 f 置为null,来让垃圾回收器进行回收。
在Javascript中,如果一个对象不再被引用,那么这个对象就会被GC回收。 如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也 会被回收。因为函数a被b引用,b又被a外的c引用,这就是为什么函数a执行后 不会被回收的原因
实现sleep
function sleep (time) {
return new Promise((resolve) => {
setTimeout(resolve, time)
})
}
// then 方式
sleep(2000).then(() => {
console.log('done')
})
// async/await 方式
async function testSleep () {
console.log('start')
await sleep(3000)
console.log('end')
}
testSleep()
在上面这个例子中,当 sleep 函数被调用时,它返回了一个 Promise 对象,内部的 setTimeout 函数创建了一个闭包,
因为它引用了外部函数 sleep 的参数 time 以及传递给 setTimeout 的回调函数 resolve。闭包优化主要思路是尽量减少不必要的闭包引用以及闭包内引用的变量能及时释放。
function sleep (time) {
let timerId
const promise = new Promise((resolve) => {
timerId = setTimeout(() => {
clearTimeout(timerId)
resolve()
}, time)
})
return promise
}
优化后: 当定时器触发时,首先通过 clearTimeout 清除定时器,这样可以确保定时器资源不会被浪费,并且相关的闭包引用也能在定时器任务完成后及时被释放。这有助于避免可能出现的内存泄漏等问题。
闭包优点和缺点
优点
- 减少全局变量的使用,保证了内部变量的安全,同时外部函数也可以访问内部函数的变量
- 在内存中维持一个变量,也可以用作缓存
缺点
- 被引用的内部变量不能被销毁,增大了内存消耗,使用不当易造成内存泄露,解决办法可以在内部变量不使用时,把外部的引用置为 null
- 闭包就是函数间的跨作用域访问,会导致性能损失
立即执行函数和闭包的区别
立即执行函数和闭包没有关系,虽然两者会经常结合在一起使用,但两者有本质的不同
立即执行函数只是函数的一种调用方式,只是声明完之后立即执行,这类函数一般都只是调用一次(可用于单例对象上),调用完之后会立即销毁,不会占用内存
闭包则主要是让外部函数可以访问内部函数的作用域,也减少了全局变量的使用,保证了内部变量的安全,但因被引用的内部变量不能被销毁,增大了内存消耗,使用不当易造成内存泄露
立即执行函数与闭包常结合示例
如:单例
let single = (function () {
let name = "小明";
let age = 20;
return {
getName: function () {
return name;
},
getAge: function () {
return age;
}
}
})();
console.log(single.getName()); //小明
console.log(single.getAge()); //20
给对象创建了私有变量name、age又对外提供了获取的方法,典型的自执行函数和闭包结合使用的示例
经典面试题 (引用其他文章)
1:问输入的结果
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
console.log(i);
答:会输出6个5,首先先输出最下边的那个5,因为for完成后 i 的值为5,然后过1秒钟再同时输出5个5,因为for设置了5的定时器,几乎会同时输出
2:问如果想输出 5 0 1 2 3 4
for (var i = 0; i < 5; i++) {
(function (i) {
setTimeout(function () {
console.log(i);
}, 1000);
})(i);
}
console.log(i);
答:第一个5很好输出,因为for循环以后i就会变成5,剩下的我们可以使用立即执行函数来做,首先 JS中调用函数传递参数都是值传递 ,所以当立即执行函数执行时,首先会把参数i的值复制一份,然后再创建函数作用域来执行函数,循环5次就会创建5个作用域,所以1秒后几乎会同时输出0 1 2 3 4
3:或者我们可以这样写
for (var i = 0; i < 5; i++) {
output(i)
}
console.log(i);
function output(i) {
setTimeout(function () {
console.log(i);
}, 1000);
}
答:这样也会先输出5,过1秒后几乎同时输出0 1 2 3 4,其实立即执行函数和现在这样的写法都是利用的JS中调用函数传递参数都是值传递的特点
4:问如何每隔1秒输出一个数字 0 1 2 3 4 5
for (var i = 0; i < 5; i++) {
(function (i) {
setTimeout(function () {
console.log(i);
}, i * 1000);
})(i);
}
setTimeout(function () {
console.log(i);
}, i * 1000);
其实很简单通过修改定时时间来完成每隔1秒输出一个数字