1.作用域
作用域主要分为:局部作用域和全局作用域。
局部作用域又分为:函数作用域和块作用域
- 函数作用域:在函数中定义的变量只能在函数内部使用,外部无法访问
- 块作用域:被大括号{}包起来的代码块,在这个代码块中定义的变量就生存在块作用域内
全局作用域:全局作用域可以被其他作用域访问
作用域链
作用域链就是变量查找的机制,在函数被调用的时候,会优先查找函数作用域内的变量。如果找不到再去依次向外层父作用域查找。
如果被调函数作用域内有与外部变量同名变量,优先调用函数作用域内部变量
作用域之间的关系:子作用域可以访问父作用域,父作用域不可访问子作用域
2.闭包
我们来看一个简单的闭包实例
function outter(){
let a =0
function inner(){
a++
console.log(a)
}
return inner
}
let key = outter()
//实际上就是 key = inner 因为outter()最后的返回值是inner
key()
//调用一次就执行一次inner()函数
这是一个简单的计数器的例子,当我们使用key接收inner的值的时候。其实就是执行了一边outter函数然后把函数的返回值赋给key。此时的key()就是执行inner函数。
按理来说变量a会随着outter函数的结束而销毁。但是实际情况却并不是,我们通过key依旧可以访问这个变量。这里就要讲一下js的垃圾回收机制了。
js的垃圾回收机制是根据变量是否被引用而决定是否销毁它。因为在inner函数中我们通过console.log引用了外部的变量a。这就导致了内部的函数引用了外部的变量。虽然外部函数结束了,但是由于这个变量依旧被引用,所以变量得以保存
详细的过程:
1.当outter函数被执行的时候,会创建一个局部变量a
2.当inner函数被定义的时候会捕获其所在的词法环境
3.outter函数执行结束,正常情况是a被销毁,但是由于inner函数的引用,让a得以保存
4.当inner函数执行的时候仍然可以访问a,因为闭包保留了a
至于为什么不直接使用变量去实现计数器的效果,是因为闭包会把一个变量变为私有变量。正常的访问是无法访问这个变量的。只有我们通过inner函数调用的时候才会访问这个变量。这在一定程度上提升了安全性。
3.变量和函数提升
变量提升
变量的声明有三种方式:1.let 2.const 3.var
前两种我们经常会用到,但是第三种我们在之前都没怎么用过。
其实let和var本质上都是差不多的,唯一的区别在一var会存在“变量提升”
变量提升就是允许变量在声明之前被访问
我们来看两个案例:
案例1
console.log(num)
let num =10
案例2
console.log(num)
var num =10
结果:案例一报错。案例二打印undefined
通过这个我们可以很明确的发现,使用var声明的变量可以在声明语句前被访问,但是访问的结果是undefined,变量在未赋值之前访问结果是undefined。这也就说明了:变量提升只会提升变量的定义,不会提升赋值。
案例2的代码就相当于:
let a
console.log(a)
a=10
我们应该避免使用var
函数提升
函数提升和变量提升基本上一样,函数提升就是把当前作用域的函数声明提升到最前面。
我们在之前的代码中可能都会把函数声明来写到后面。
按照之前的思路来说,应该是先声明在使用。但是函数自己的定义就会有提升的效果。也就是说函数默认有函数提升。
我们之后在编写代码的时候如果有函数需要单独的写出来,我们建议写在最前面。
4.函数相关(参数、箭头函数
函数参数
动态参数
我们在编写代码的时候一般都会提前写好有几个参数,但是有的时候我们并不能确定我这个函数需要几个参数。这个时候再编写函数的代码的时候就会非常困难。
但是我们有一种方法可以解决这个问题:函数内置的arguments伪数组。
function fn(){
console.log(arguments.length)
console.log(arguments[0], arguments[1], arguments[2])
console.log(typeof arguments[0], typeof arguments[1], typeof arguments[2])
}
fn(1,1.1,"Www")
看这段代码,我的函数在定义的时候是没有声明形参的,但是我调用的时候却传入了三个实参。
然后我通过arguments这个函数内置的伪数组得到了这三个参数。
arguments是函数内置的伪数组,它可以存储调用函数的时候传入的参数。即使函数定义了形参我依旧可以通过arguments获取这个形参
剩余参数
剩余参数:我们在传参的时候一般都是一一对应的传参,如果传递的实参数量大于形参,剩余的没有对应的参数就是剩余参数。我们可以把这些剩余参数表示为一个真数组
下面看案例来理解:
function fn(num,...arr){
console.log(num);
console.log(arr);
}
fn(1,2,3,4,5,7)
最后输出num是1,arr是一个数组,数组包含2,3,4,5,7
这个arr就是一个剩余参数构成的数组,一般来说我们都会把剩余参数写在参数列表的后面。
展开运算符:...数组 -》可以把数组的所有元素拿出来
假设有一个数组arr=[1,2,3,4,5,6,7] 则 ...arr = 1,2,3,4,5,6,7(不是字符串
箭头函数
箭头函数基本形式
(参数)=>{函数体}
1.如果参数只有一个 :可以省略参数的小括号: 参数=>{函数体}
2.如果函数体只有一条语句:可以省略大括号: (参数)=>语句 并且这个语句的结果就是返回值
箭头函数的this --- 继承父级的this
一个对象里面的方法(普通函数)的this指向这个对象,因为是对象调用了这个方法
一个对象里面的方法(箭头函数)的this指向window,因为箭头函数的this继承了对象的this
数组解构
作用:快速批量赋值
用法1.
arr = [1,2,3,4,5,6,7]
const [a,b,c,d,e,f,g] = arr 或者 const [a,b,c,d,e,f,g] = [1,2,3,4,5,6,7]
用法2.
有两个变量,想交换它们的值:[a,b] = [b,a]
注:const [a,b,c] = [1,2]这种情况下c是undefined
const [a,b] = [1,2,3]这种情况下会舍去多余的
可以使用剩余参数防止丢失:const[a,b,...c] = [1,2,3,4,5,6,7,8,9] 这种情况a1,b2,c是数组
对象解构
对象解构的作用和数组结构一样
我们先来回顾以下对象
对象名={对象内容},对象内容可以是变量或者函数,变量称为属性,函数称为方法。中间使用逗号隔开
obj ={
name:"www",
age:11,
outer:function(){
console.log(this.name,this.age)
}
}
const {name,age,outer} = obj
console.log(name,age)
outer()
具体使用没什么区别,然后要注意的是数组使用[]对象使用{}
方法结构出来可以调用它。但是outer是不能输出我们想要的内容的,主要是解构出来之后调用者从obj变成了window。
变量的名字要和属性名一致
如果想让变量不和属性一个名字可以使用const{name:aaa}这种方式让name变成aaa
构造函数
我们首先讲一下new关键字,这个关键字用于创建一个空对象
这个空的对象里面什么都没有,我们有两个方法为这个对象添加属性或者方法
1.new 构造函数()
2.new一个对象,通过对象.属性/方法添加
这是通过构造函数:
构造函数的作用就是创建和初始化对象。他需要使用new关键字调用
下面是一个简单的构造函数:
function student(stu_name,stu_age,stu_gender,stu_add){
this.name=stu_name;
this.age=stu_age;
this.gender=stu_gender;
this.add=stu_add;
}
const stu1= new student("xxx",11,"nan","001100")
console.log(stu1)
我们首先声明了一个构造函数,这个函数接收四个参数
通常来说,这个函数里面的this指的是window。但是通过new关键字调用构造函数会把new关键字创建的新对象绑定到构造函数的this。
我们只需要记住通过new关键字调用构造函数,构造函数的this指向new创建的对象
其实我们在构造函数里面打印一下this就能知道this指向的是谁了。上面的例子里打印this会打印出student
这是通过手动添加:
我们需要先创建一个空的对象:const obj = new Object()
然后obj.属性/方法 添加属性或者方法