1.作用域
JavaScript是函数级作用域编程语言:变量只在其定义时所在的function内部有意义。
js
function fn(){
let a = 10; //变量a在fn函数内部定义 fn就是a的作用域 变量a被称为局部变量
}
fn();
console.log(a); //报错
全局变量
如果不将变量定义在任何函数的内部,此时这个变量就是全局变量,它在任何函数内都可以被访问和
更改。
js
let a;
function fn(){
a = 10;
a++;
console.log(a);
}
fn();
console.log(a);
遮蔽效应
如果函数中也定义了和全局同名的变量,则函数内的变量会将全局的变量“遮蔽”
js
//全局变量
let a = 10;
function fn(){
//局部变量,会把全局变量a遮蔽
let a = 666;
a++;
console.log(a);
}
fn(); //667
console.log(a); //10
注意变量提升的情况
js
var a = 10;
function fn(){
a++; //局部变量a被自增 1 a此时是undefined 自增1结果是NaN
var a = 20;
console.log(a); //20
}
fn();
console.log(a); // 10
形参也是局部变量
js
let a = 10;
function fn(a) {
a++;
console.log(a);
}
fn(699); //700
console.log(a); //10
作用域链
函数的嵌套
一个函数内部也可以定义一个函数。
和局部变量类似,定义在一个函数内部的函数是局部函数。
js
function fn(){
function inner(){
console.log("tianDSB");
}
inner(); //调用内部函数
}
fn(); //调用外部函数
// inner(); 报错
在函数嵌套中,变量会从内到外逐层寻找它的定义。
js
let a = 10;
let b = 20;
function fn() {
let c = 30;
function inner() {
let a = 40;
let d = 50;
console.log(a, b, c, d); //使用变量时 Js会从当前层开始,逐层向上寻找定义
}
inner();
}
fn(); //40 20 30 50
//先从自身的AO局部作用域开始寻找 自身有用自己 没有的话一层层往上找 直到全局作用域GO
函数的作用域 是定义函数的时候决定的
而不是执行函数时候决定的
js
let a = 10;
function f(){
console.log(a);
}
function ff(){
let a = 20;
f();
}
ff();//10
不加let将定义全局变量
在初次给变量赋值时,如果没有加var,则将定义全局变量
js
function fn(){
a = 666;
}
fn();
console.log(a);
js
let a = 1;
let b = 2;
function fn(){
//字母c没有加关键字定义,所以它就变为了全局变量
c = 3;
c ++;
let b = 4;
b++;
console.log(b);
}
fn();
console.log(b);
//函数外部可以访问变量C
console.log(c);
2.var let const
ES6之前的作用域
全局作用域
函数作用域
块级作用域
let const 有块级作用域
通俗的讲就是一对花括号中的区域
{} for(){} while(){} do{}while() if(){} switch(){}
块级作用域可以嵌套
let变量
let 声明 变量 代替 var
1.let声明的变量只在当前(块级)作用域内有效
js
{
let age = 18;
console.log(age); //18
}
console.log(age); //报错
js
if(true){
var a = 10;
let b = 20;
}
console.log(a);//10
console.log(b);//报错
js
let b = 666;
if(true){
var a = 10;
let b = 20;
}
console.log(a);//10
console.log(b);//666
js
for(let i = 0 ; i<3; i++){
let a = 20;
}
console.log(a);//报错
console.log(i);//报错
2.使用let const 声明的变量 不能重新被声明
var 允许重复声明 let const不允许
let
不允许在相同作用域内,已经存在的变量和常量 又声明一遍 ;
js
var name1 = "我是tian";
var name1 = "我是夕颜"
console.log(name);//我是夕颜
js
var name2 = "tian";
let name2 = "夕颜";//报错
js
let name2 = "我也是tian";u
let name2 = "我也是夕颜"; //报错
js
function fn(a){
let a = 1;
}
fn(2); //报错
因此,不能在函数内部重新声明参数。
3.let不存在变量提升
var 会提升变量的声明到当前作用域的顶部
js
console.log(n);//undefined
var n = "tian";
js
console.log(n);//报错
let n = "tian";
4.暂时性死区(TDZ)
let const 定义存在 暂存死区 禁止在声明的位置前访问
只要作用域内有 let const 他们所声明的变量或者常量就会自动"绑定"在这个区域,不再受外部作用域的影响
js
let a = 2;
function fn(){
console.log(a);
let a = 1;
}
fn(); //报错 内部函数定义了a 绑定这个作用域 你只要找a变量只会在当前作用域去查找
//养成良好的编程习惯,对于所有的变量或者常量,做到先声明,后使用
5.window上面的属性和方法
全局作用域中,var声明的变量,通过function声明的函数,会自动变成 window对象的属性或方法
let const不会
全局作用域的本质是全局对象的属性
览器中全局对象是window,我们申明的变量都相当于在全局对象window下添加属性
js
var age = 18;
console.log(window.age); //18
function fn(){}
console.log(window.fn);
console.log(window.fn === fn); //true
let b = 2
console.info(window.b) // undefined
const 常量
常量必须在声明的时候赋值 目的就是为了那些一旦初始化就不希望重新赋值的情况设计的
注意
使用const必须在声明的时候赋值 不能留到以后赋值 其他特性参考let
js
const a; //报错
const允许在不重新赋值的情况下修改它的值
主要针对引用数据类型
js
//基本数据类型
const name = "tian";
name = "夕颜"; //报错
js
//引用数据类型
const obj = {
name: "tian"
}
//obj = {}; 报错
obj.name = "夕颜";
console.log(obj);
3.预解析
js
console.log(a) // undefined (没有报错,而是输出了undefined)
var a = 10;
console.info(a) // 10
函数声明和函数表达式的区别
js
a();//函数声明允许提前调用
b();//通过 = 赋值定义的函数 不允许提前使用
function a(){
console.log("a函数");
}
let b = function(){
console.log("b函数");
}
解析顺序可以整体的分为两步,第一步定义,第二步执行。
定义过程
找到当前作用域所有的var声明的变量名,
找到当前作用域function定义的有名函数块。
提升在最开头
注意
var声明的变量名提升此时仅仅只是变量名,
后面的 = 号不会在这一步执行,也就是说在这一步,所有var的变量都是初始值undefined。
执行过程
会从上到下的执行代码,也就是我们传统理解的那样了
变量提升只在当前作用域执行:
如果在执行过程中,执行了函数,
那么就会开辟一个新的子作用域,此时会进入新的作用域解析里面的代码,
同样的也遵循上述的两项解析步骤。
js
var x = 10;
fn();
console.log(x);//10
function fn(){
var x = 20;
}
/*
以上代码的执行步骤为:
首先进行变量提升
var x;
function fn(){ alert(1) }
之后再进行赋值和函数执行
x = 10;
fn();
fn ==> 产生新的作用域 ==> 1.定义 var x ;
2.执行 x = 20;(当前作用域的x)
console.log(x) // 10
*/
变量/函数 重复定义
var var重名只留一个
function function重名留后面的
var function重名留function ( 不管定义顺序如何 function 都会覆盖同名的变量 )
js
var x = 10;
var x = 20;
console.log(x) // 20
js
console.log(y); // function y(){ alert(2) }
function y(){ alert(1) }
function y(){ alert(2) }
console.log(y) // function y(){ alert(2) }
js
console.log(z); // function z(){ alert(1) }
function z() {
alert(1);
}
var z = 10;
小练习
题目1
js
var x = 5;
a();
function a(){
alert(x);//undefined
var x = 10;
}
alert(x);//5
/*
1.定义
var x;
function a(){...}
2.执行
x = 5;
a() ===> 新的作用域 ===> 1.定义
var x;
2.执行
alert(x);//undefined
x = 10;(不影响全局)
alert(x);//5
*/
题目2
js
a();
function a(){
alert(x);//undefined
var x = 10;
}
alert(x);//报错
/*
1.定义
function a(){...}
2.执行
a() ===> 新的作用域 ===> 1.定义
var x;
2.执行
alert(x)//undefined
alert(x)//报错
*/
题目3
js
function a(){
alert(1);
}
var a;
alert(a);//函数体
题目4
js
alert(a);//函数体
var a = 10;
alert(a);//10
function a(){alert(20)}
alert(a);//10
var a = 30;
alert(a);//30
function a(){alert(40)}
alert(a);//30
/*
1.定义
function a(){alert(40)}
2.执行
alert(a);函数体
a = 10;(当前作用域有a 修改自己的a)
alert(a);//10
alert(a);//10
a = 30;(当前作用域有a 修改自己的a)
alert(a);//30
alert(a);//30
*/
题目5
js
var a = 10;
alert(a);//10
a();//报错
function a(){
alert(20);
}
/*
1.定义
function a(){...}
2.执行
a = 10;
alert(a);//10
a();//报错
*/
题目6
js
a();//2
var a = function(){alert(1)};
a();//1
function a(){alert(2)};
a();//1
var a = function(){alert(3)};
a();//3
/*
1.定义
function a(){alert(2)};
2.执行
a();//2
a = function(){alert(1)};
a();//1
a();//1
a = function(){alert(3)};
a();//3
*/
题目7
js
var a = 10;
function fn() {
alert(a);//undefined
var a = 1;
alert(a);//1
}
fn();
alert(a);//10
/*
1.定义
var a;
function fn() {...}
2.执行
a = 10;
fn() ==> 新的作用域 ===> 1.定义
var a;
2.执行
alert(a);//undefined
a=1;
alert(a);//1
alert(a);//10
*/
题目8
js
fn();
alert(a);//undefined
var a = 10;
alert(a);//10
function fn(){
var a = 1;
}
/*
1.定义
var a;
function fn(){}
2.执行
fn(); ==> 新的作用域 ===> 1.定义
var a;
2.执行
a = 1;(当前作用域有a 修改自己的a)
alert(a);//undefined
a = 10;
alert(a);//10
*/
4.垃圾回收机制
JavaScript自动回收不再使用的变量,释放其所占用的内存,开发者不需要手动做垃圾回收的处理。
Javascript 会找出不再使用的变量,不再使用意味着这个变量生命周期的结束。
Javascript 中存在两种变量——全局变量和局部变量,全部变量的声明周期会一直持续,直到页面卸载,所以当我们定义了一个全局的对象时,使用完毕之后,最好给它重新赋值为null,以便释放它所占的内存(这个变量没有被回收,只是改变了指向,减少内存占用。)
但是有一种情况的局部变量不会随着函数的结束而被回收,那就是局部变量被函数外部的变量所使用,其中一种情况就是闭包
垃圾回收的两种实现方式
垃圾回收有两种实现方式,分别是标记清除和引用计数
标记清除:
当某个变量不再被使用时,该变量就会被回收。
当变量进入执行环境时标记为“进入环境”,当变量离开执行环境时则标记为“离开环境”,
被标记为“进入环境”的变量是不能被回收的,因为它们正在被使用,
而标记为“离开环境”的变量则可以被回收
js
function fn1 () {
let a = 1;
let b = 2;
// 函数执行时,a b 分别被标记 进入环境
}
fn1(); // 函数执行结束,a b 被标记 离开环境,被回收
引用计数:
极少数浏览器(如 IE object-c)上针对引用类型的回收机制
统计引用类型变量声明后被引用的次数,当次数为 0 时,该变量将被回收
当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1
如果这个变量的值又被赋值给了另外一个变量(即两个变量的地址都指向同一个引用类型),则该值的引用次数+1。
相反,如果包含这个引用类型的变量又取了另外一个值,则引用次数 - 1。
当这个引用类型的引用次数变为 0 时,则说明无法再访问这个变量。
当垃圾收集器下次再运行时,它就会释放引用次数为 0 的值所占用的内存。
js
let xm = {
name : 'xm',
age:18
}//1
let xh = xm ;//2
xh = {};//1
xm = {};//0
但是引用计数的方式,有一个相对明显的缺点——循环引用
js
function fn(){
let xm = {};//1
let xh = {};//1
}
fn();//fn里面的xm xh是局部变量使用完了之后会把xmxh默认设置为 null
xm = null;
xh = null;
js
function fn(){
let xm ={}; //1
let xh = {}; //1
xm.wife = xh;//2
xh.husband = xm;//2
}
fn();
xm = null; //1
xm = null; //1
//此时这个变量不会被回收
//像上面这种情况就需要手动将变量的内存释放
xm.wife = null
xh.husband = null
js
let x = 10;
function fn(){
let xx = 100;
console.log(xx);//xx(局部变量)被使用完了之后,就会被垃圾回收机制回收
}
fn();
//全局变量永远不回收(关闭当前网页回收)
在现代浏览器中,Javascript 使用的方式是标记清除,所以我们无需担心循环引用的问题
5.闭包
从一个题目开始
js
//创建一个函数
function fn(){
//定义局部变量
let name = "tian";
//定义一个局部函数
function innerFun(){
alert(name);
}
return innerFun; //返回了内部函数
}
//定义一个全局变量
let name = "夕颜";
let inner = fn(); // 内部函数被移动到了外部执行
//执行inner函数,就相当于在fn函数的外部,执行了内部的函数
inner();
什么是闭包
JavaScript中函数会产生闭包(closure)。闭包是函数本身和该函数声明时所处的环境状态的组合。
**函数能够“记忆住”其定义时所处的环境,**即使函数不在其定义的环境中被调用,也能访问定义时所处环境的变量。
闭包: 一个函数中嵌套另一个函数,内部函数使用了外部函数的参数或变量,就构成了闭包(这个内部函数就叫做闭包)
或者 父级作用域 包裹了 子作用域 子作用域使用了父作用域的变量/参数
闭包使得局部的变量/参数 不会被回收
在JavaScript中,每次创建函数时都会创建闭包。
但是,闭包特性往往需要将函数“换一个地方”执行,能被观察出来。
js
function fn1(){
//父作用域
let x = 10;
function fn2(){
//子作用域
x++;
console.log(x);
}
}
//父级作用域不能是全局 全局没有闭包的概念
//上述代码 子作用域使用了父作用域的变量 构成闭包
js
let x = 10;
function fn() {
let a = 666;
a++;
console.log(a); //局部变量a使用完了之后就会被垃圾回收机制回收
}
fn(); //667
fn(); //667
js
let x = 20;
function fn(){
x++;
}
fn();
//虽然全局变量不会被回收,但是会造成命名污染
闭包非常实用
闭包很有用,因为它允许我们将数据与操作该数据的函数关联起来。这与“面向对象编程”有少许相似之处。
闭包的功能:记忆性、模拟私有变量。
闭包用途1-记忆性
当闭包产生时,函数所处环境的状态会始终保持在内存中,不会在外层函数调用后被自动清除。这就是闭包的记忆性。
闭包的记忆性举例
创建体温检测函数checkTemp(n),可以检查体温 n 是否正常,函数会返回对应提示。
但是,不同的小区有不同的体温检测标准,比如A小区体温合格线是37.1度,而B小区体温合格线是37.3度,应该如何编程?
html
<script>
function createCheckTemp(standardTemp){
function checkTemp(n){
if(n >= standardTemp){
alert("你的体温偏高");
}else{
alert("体温正常,可以通过");
}
}
return checkTemp;
}
//创建一个checkTemp函数,它以37.1度为标准线
let checkTemp_A = createCheckTemp(37.1);
// checkTemp_A(37.2);
//创建一个checkTemp函数,它以37.3度为标准线
let checkTemp_B = createCheckTemp(37.3);
checkTemp_B(37.2)
</script>
闭包用途2–模拟私有变量
题目:请定义一个变量a,要求是能保证这个a只能被进行指定操作(如加1、乘2),而不能进行其他操作,应该怎么编程呢?
在Java、C++等语言中,有私有属性的概念,但是JavaScript中只能用闭包来模拟。
html
<script>
//封装一个函数 这个函数的功能就是私有化变量
function fn() {
//定义一个局部变量a
let a = 1;
return {
getA: function () {
return a;
},
addA: function () {
a++;
},
subA: function () {
a--;
},
pow: function () {
a *= 2;
}
};
}
let obj = fn();
//如果想在fn函数外面使用变量a,唯一的方法就是调用getA()方法
console.log(obj.getA());
obj.addA();
obj.pow();
obj.addA();
console.log(obj.getA());
</script>
使用闭包的注意点
不能滥用闭包,否则会造成网页的性能问题,严重时可能导致内存泄露。
所谓内存泄漏是指程序中不再被需要的内存, 由于某种原因, 无法被释放
闭包一道面试题
html
<script>
function addCount() {
let count = 0;
return function () {
count = count + 1;
console.log(count);
};
}
let fn1 = addCount();
let fn2 = addCount();
fn1();
fn2();
fn2();
fn1();
</script>
事件函数里面为什么建议使用this
html
<style>
#wrap{
width: 100px;
height: 100px;
background-color: pink;
}
</style>
<body>
<div id="wrap"></div>
<script>
(function(){
let oWrap = document.getElementById("wrap");
oWrap.onclick = function(){
this.innerHTML = "tian丫";
}
})();
//换成this 此时子作用域没有引用父作用域的变量 oWrap就可以被回收
</script>
</body>
6.IIFE
llFE(Immediately Invoked Function Expression,立即调用函数表达式)是一种特殊的JavaScript函数
写法,一旦被定义,就立即被调用。
js
(function ()
statements
})();
//外边括号会将函数变为表达式 后面直接加括号运行函数
这种类型的立即执行函数其实可以从计算公式去理解
js
function add(){
return 3;
}
let a= 1 + add(); //a结果是4 add函数执行一次然后将返回值用来计算,
//那么我们去掉1
var b = +add();//b就等于3
//所以 +add()就变成+function(){return 3;}();
//这就是立执行函数的原理
作用
让每个模块独立出来 ,不必为函数命名,避免了污染全局变量
可以封装一些外部无法读取的私有变量。
有时,我们需要在定义函数之后,立即调用该函数。
这时,你不能在函数的定义之后加上圆括号,这会产生语法错误。
js
function f(){ /* code */ }();
//报错
//JavaScript 规定,如果function关键字出现在行首,一律解释成语句
//引擎看到行首是function关键字之后,认为这一段都是函数的定义,不应该以圆括号结尾,所以就报错
解决方法就是不要让function
出现在行首,让引擎将其理解成一个表达式。
js
let b = function(){
console.log("b函数");
}();
//函数表达式可以加括号自执行 这个写法很奇怪
还有哪些方式可以将函数变成函数表达式呢?
最简单的处理
就是将其放在一个圆括号里面。
js
(function () {
console.log("111")
})();
//以圆括号开头,引擎就会认为后面跟的是一个表示式而不是函数定义语句,所以就避免了错误
//(Immediately-Invoked Function Expression),简称 IIFE。
其他写法
js
(function () {/* code */ } ());
!function () { /* code */ }();
~function () { /* code */ }();
-function () { /* code */ }();
+function () { /* code */ }();
注意,上面两种写法最后的分号都是必须的。如果省略分号,遇到连着两个 IIFE,可能就会报错
js
// 报错
(function(){ /* code */ }())
(function(){ /* code */ }())
//上面代码的两行之间没有分号,JavaScript 会将它们连在一起解释,将第二行解释为第一行的参数。
IIFE的作用
1.为变量赋值
语法显得紧凑
js
let age = 6;
let sex = "男";
let title;
if (age < 18) {
title = "小朋友";
} else {
if (sex == "男") {
title = "先生";
} else {
title = "女士";
}
}
//代码不紧凑
let title = (function(){
if(age < 18){
return "小朋友";
}else{
if(sex == "男"){
return "先生";
}else{
return "女士";
}
}
})();
alert(title);
2.将全局变量变为局部变量
IFE可以在一些场合(如for循环中)将全局变量变为局部变量,形成闭包
html
<script>
let arr = [];
for(var i = 0; i < 5; i++){
arr.push(function(){
console.log(i); //变量i是全局变量 , 所有函数都共享内存中的同一个变量i
});
}
arr[0]();
arr[1]();
arr[2]();
arr[3]();//5
arr[4]();
//使用IIFE 或者把var改成let产生块级作用域
let arr = [];
for (var i = 0; i < 5; i++) {
(function (index) {
arr.push(function () {
console.log(index);
});
})(i);
}
</script>
html
<style>
div {
width: 300px;
height: 300px;
background-color: rgba(0, 0, 0, 0.4);
margin: 100px auto 0;
font-size: 50px;
color: #fff;
text-align: center;
line-height: 300px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="one">one</div>
<div class="two">two</div>
<script>
(function () {
let one = document.querySelector(".one");
let index = 0;
one.onclick = function () {
console.log(index++);
};
})();
(function () {
let two = document.querySelector(".two");
let index = 0;
two.onclick = function () {
console.log(index++);
};
})();
</script>
let const的应用
html
<style>
* {
margin: 0;
padding: 0;
}
body {
height: 100vh;
display: flex;
justify-content: space-evenly;
align-items: center;
}
button {
width: 200px;
height: 200px;
font-size: 100px;
cursor: pointer;
}
</style>
</head>
<body>
<button>0</button>
<button>1</button>
<button>2</button>
<script>
let btn = document.querySelectorAll("button");
for (var i = 0; i < btn.length; i++) {
btn[i].onclick = function () {
console.log(i); //每次点击都打印4
};
}
// for循环产生3个事件函数都使用了全局的同一个i(循环结束的i)
</script>
js
//使用IIFE形成闭包 保存变量
for (var i = 0; i < btn.length; i++) {
(function (index) {
btn[i].onclick = function () {
console.log(index);
};
})(i);
}
js
//使用let 形成块级作用域
for (let i = 0; i < btn.length; i++) {
btn[i].onclick = function () {
console.log(i);
};
}
//let直接产生一个作用域 和里面的事件函数 构成闭包 i不会被回收