这次学的有点断断续续的,排版有点乱 😃
[TOC]
作用域
- 区分LHS 和 RHS
1.LHS -> 左查询 -> 查询是为了赋值
2.RHS -> 非左查询 -> 为了查到变量的值
// 例1:
var a = 1;
// 例2:
- RHS查询demo
- LHS查询num,将2赋值num
- RHS查询num的值
function demo(num){
console.log(num)
}
demo(2)
// 例3:
console.log(a)
- LHS 和 RHS 异常报错
- 若 RHS 查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError异常;
- 若引擎执行 LHS 查询时,在顶层(全局作用域)中也无法找到目标变量,“严格模式”下,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎;“严格模式下”,禁止自动或隐式地创建全局变量,LHS 查询失败时,并不会创建并返回一个全局变量,引擎会抛出ReferenceError异常;
- 若RHS 查询找到了一个变量,但是你尝试对这个变量的值进行不合理的操作,
比如试图对一个非函数类型的值进行函数调用,或着引用 null 或 undefined 类型的值中的属性,那么引擎会抛出 TypeError;
词法作用域
词法作用域:由定义变量或者函数声明的位置决定
- 举例
- b的词法作用域即函数a()的局部作用域
- 变量a是全局变量 => 函数aFun,bFun,cFun均可以访问
- 变量c是局部变量 => 函数aFun,bFun可以访问
var a
function aFun(){
var c
function bFun(){
var d
console.log(1)
}
}
function cFun(){
}
- 未声明即使用的变量是全局变量,注意任何位置均可访问的问题
函数作用域和块作用域
var声明的变量都会成为全局变量
let
- let会为其声明的变量隐式绑定所在的块作用域,通常是{...}内部(eg.1)
- 开发时最好为块作用域显式地创建块, 使变量的附属关系变得更加清晰(eg.2)
- 块作用域非常有用的原因和闭包及回收内存垃圾的回收机制相关 (eg.3.1 3.2),
- 原本process执行之后process(..) 执行后,someReallyBigData变量占用的内存就可以销毁,
- 但是, click 函数形成了一个覆盖整个作用域的闭包,someReallyBigData变量虽然没有使用,JavaScript 引擎极有可能依然保存着这个结构(取决于具体实现),添加了块级作用域后,让引擎清楚地知道没有必要继续保存 someReallyBigDat变量;
// eg.1
for(let i = 0;i<3;i++){
...
}
console.log(i) // ReferenceError
// eg.2
if(foo){
{ // => 显式的块
let bar = 1;
console.log(bar);
}
}
// eg.3.1
function process(data) {
...
}
var someReallyBigData = { .. };
process( someReallyBigData );
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt) {
console.log("hello");
});
// eg.3.2
function process(data) {
...
}
{
// 在这个块中定义的内容可以销毁了!
let someReallyBigData = { .. };
process( someReallyBigData );
}
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt){
console.log("hello");
});
函数声明和函数表达式
区分:看function 的位置 (function 是声明中的第一个词,那么就是一个函数声明 )
区别:名称标识符将会绑定在何处:
- a : 被绑定在全局作用域
- b : 被绑定在函数表达式自身的函数中, 即 (...)所在的位置中访问,外部作用域则不行,b 变量名被隐藏在自身中意味着不会污染外部作用域
// 函数声明
function a(){
console.log(1)
}
// 函数表达式
(function b(){
...
})
块级作用域
立即执行函数表达式(IIFE)
由于函数被包括在一对()括号内部,第一个()使之成为一个表达式,第二个()执行了这个函数
优点:
- 不污染全局作用域
- 不需显式的通过函数名调用这个函数
// 写法一:
(function(){})()
// 写法二:
(function(){}())
// 函数表达式
var a = 2;
(function foo() {
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2
foo() // 报错:Uncaught ReferenceError
IIFE进阶用法
- 将IIFE当作函数调用并传递参数进去(eg.1)
// eg.1
// 实参:window
// 形参:global
var a = 2;
(function IIFE(global){
var a =3;
console.log(a); // 3
console.log(global.a) // 2
})(window)
什么是闭包?
当函数可以 记住并访问 其所在的 词法作用域 ,使函数在他本身词法作用域以外执行,就产生了闭包;
- 内部函数 showA 在被执行之前就被返回, 函数 showA 在定义的词法作用域以外被调用;
- 由于函数 showA 占用了函数 result 的变量a, 导致函数a执行完毕后,函数result() 的内存空间不会被垃圾回收机制清除;
- 函数 showA 依然持有函数result()函数作用域的引用,这个引用就叫做闭包;
- 闭包使得函数可以继续访问定义时的词法作用域;
// eg.1
function result() {
var a = 1;
function showA() {
console.log(a)
}
return showA
}
result()() // 1
闭包应用场景举例
无论以何种方式对函数类型的值进行传递,在函数在别处调用时都可以观察到闭包
// 可以是在全局作用域调用局部函数
function a() {
var cc = 1
function b() {
console.log(cc);
}
return b
}
const fn = a()
fn() // 1
// 也可以是局部作用域调用另一个局部作用域的函数
function a() {
var cc = 1
function c() {
console.log(cc)
}
b(c)
}
function b(fn) {
fn()
}
a()
开发中常见闭包应用场景举例
1. setTimeout()
function wait(message) {
setTimeout( function timer() {
console.log( message );
}, 1000);
}
wait( "Hello,hello!" );
1.timer 还持有变量message的引用 ,因此形成涵盖wait()函数作用域的闭包;
2.wait() 执行1s后,他的内部作用域并不会消失,timer函数依然保有wait()作用域的闭包
2. for循环和setTimeout拿到正确的值问题
for(var i=0;i<10;i++){
setTimeout(function(){
console.log(i)//10个10
},1000)
}
预想:每秒一次,每次一个,打印出1--10
实际:1秒后一次打印出10个10
出现原因
- setTimeout的参数一直持有变量i,形成了闭包,
- var 声明的i是全局变量
- 每次for循环就是将定时器(微任务)加入微任务队列,for循环之后依次执行微任务队列中的任务,而此时的i==10;
- 任务队列的十个setTimeout 共享同一个词法作用域,由于循环在定时任务触发之前就已经执行完毕,由于var声明变量具有变量提升的特点,此时的i===10,因此每次取出的的setTimeout任务访问到的i的值都是10
?setTimeout是1s后将任务加入任务队列还是立即加入任务队列,去队列中拿出来任务,等1s 再执行?
解决方法:使用let
let会产生局部作用域
let 不仅将 i 绑定到了 for 循环的块中,事实上循环的每一个迭代它将重新绑定,确保使用上一个循环迭代结束时的值重新进行赋值
for(var i=0;i<10;i++){
setTimeout(function(){
console.log(i)//10个10
},1000)
}
上边的代码执行顺序相当于
let a = 1;
console.log(a)
a = 2;
console.log(a)
a = 3;
console.log(a)
3. 闭包和for循环问题(同二)
闭包只能取得包含函数中任何变量的最后一个值
function a() {
var arr = new Array()
for (var i = 0; i < 10; i++) {
arr[i] = function () {
return i
}
}
return arr
}
console.log(a()[0]()) // 10
console.log(a()[1]()) // 10
console.log(a()[2]()) // 10
4. 使用闭包模拟私有方法和变量
// 注意与单例模式进行区分
// 区分:todo:闭包各自维护自己的内存空间
function demo() {
var a = 1
return {
add1: function () {
a += 1;
return a;
},
sub1: function () {
a -= 1;
return a;
},
aValue: function () {
return a
}
}
}
var Demo1 = demo()
var Demo2 = demo()
console.log(Demo1.add1()) // 2
console.log(Demo2.add1()) // 3
模拟了面向对象编程的样子,实现数据隐藏和封装
Demo1 和Demo2 各自维护独立的各自独立的词法作用域,同时引用的是自己词法作用域的变量a
每次调用 demo 的时候,通过改变这个变量的值,会改变这个闭包的词法作用域,然而在一个闭包内对变量的修改,并不会影响到另一个闭包中的变量
5. 使用闭包设置单例模式
什么是单例模式?
- 保证一个类只有一个实例
常见的单例模式应用场景:
- windows的task manger(任务管理器)(非JS)
- 网站的计数器,一般也是采用单例模式实现,否则难以同步
// todo :单例模式仅实例化一次
var demo = (function () {
var a = 1
return {
add1: function () {
a += 1;
return a;
},
sub1: function () {
a -= 1;
return a;
},
aValue: function () {
return a
}
}
}())
var demo2 = demo
console.log(demo.add1()) // 2
console.log(demo2.add1()) // 3
6. 内存泄漏问题
什么是内存泄漏?
- 程序运行需要内存,只要程序提出需求,操作系统就必须提供内存,对于持续运行的服务进程,必须及时释放不再使用的内存,否则,内存占用越来越高, 轻则影响系统性能,重则导致进程崩溃;
- 不再用到的内存,没有及时释放,就叫做内存泄漏;
闭包会导致内存泄漏?
- 不会
- 由于IE9之前的版本对JScript对象和COM对象使用不同的垃圾收集,从而导致内存无法进行回收,所以可能导致内存泄漏;
- 由于闭包会使函数中的变量一直保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题;
7. 闭包实现数据问题,定时手动销毁
8. 函数防抖
9. 常见闭包中this的问题
10. 闭包常见两个面试题
// 题目1
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()()); // "The Window"
// 题目2
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()()); // "My Object"
使用闭包注意
性能
- 使用完成之后,记得手动清楚 赋值=null ,以免一直占用内存 (即内存泄漏问题)
- 闭包会携带包含他的函数作用域,因此会比其他函数占用更多的内存,过度使用可能会导致内存占用过多;