JS的作用域

全局作用域

在编程语言中,变量一般会分为全局变量和局部变量。在 JavaScript 中,全局变量是挂载在 window 对象下的变量,所以在网页中的任何位置都可以使用并且访问到这个全局变量。下面来看一下全局作用域:

var globalName = 'global';
function getName() { 
  console.log(globalName) // global
  var name = 'inner'
  console.log(name) // inner
} 
getName();
console.log(name); 
console.log(globalName); //global

可以看到,globalName 变量在任何地方都是可以被访问到的,所以它就是全局变量。而在 getName 函数中作为局部变量的 name 变量是不具备这种能力的,这就涉及待会儿说的局部作用域。

在 JavaScript 中,所有没有经过定义而直接被赋值的变量默认就是一个全局变量,比如下面代码中 setName 函数里面的 vName:

function setName(){ 
  vName = 'setName';
}
setName();
console.log(vName); // setName
console.log(window.vName) // setName

从上面可以了,全局变量是拥有全局的作用域,无论在何处都可以使用它,在浏览器控制台输入 window.vName 时,就可以访问到 window 上的全局变量,但是记住一点永远不要给没有定义的变量直接赋值。

所有的全局变量、全局函数都会附加到全局对象

这称之为全局污染,又称之为全局暴露,或简称污染、暴露

如果要避免污染,需要使用立即执行函数改变其作用域

局部作用域

函数中定义的变量,只有在函数内部可以被访问,它的作用域也就是函数的内部,称为函数作用域,代码理解一下:

function getDemo(){
	var name = 'hello'
	console.log(name)//hello
}
getDemo()
console.log(name)//报错name is not defined

可以看到,name变量是在getDemo函数中进行定义的,所以name是一个局部变量,它只能在函数内部才能被访问,在外部访问直接报错

块级作用域

ES6 中新增了块级作用域,最直接的表现就是新增的 let 和 const 关键词,使用这两个关键词定义的变量只能在块级作用域中被访问,有“暂时性死区”的特点,也就是说这个变量在定义之前是不能被使用的。

说到暂时性死区,还要从“变量提升”说起,话不多说上代码:

function demo (){
	console.log(num)
	var num = 3
}
demo()
//以上代码输出undefine,原因是变量num在函数内部做了提升
//相当于以下代码
function demo(){
	var num
	console.log(num)
	num = 3
}
demo()

使用 let 或 const 声明变量,会针对这个变量形成一个封闭的块级作用域,在这个块级作用域当中,如果在声明变量前访问该变量,就会报 referenceError 错误;如果在声明变量后访问,则可以正常获取变量值:

function demo() { 
  let bar = 3 
  console.log(bar) 
} 
demo()

这段代码正常输出 3。因此在相应花括号形成的作用域中,存在一个“死区”,起始于函数开头,终止于相关变量声明的一行。在这个范围内无法访问 let 或 const 声明的变量。

另外在 JavaScript 编码过程中, if 语句及 for 语句后面 {} 这里面所包括的也是块级作用域:

console.log(a) //a is not defined
if(true){
  let a = '123';
  console.log(a)// 123
}
console.log(a) //a is not defined

它们之间的关系

在函数中定义的变量也可以嵌套在其他函数的作用域中,可以理解成一个作用域链,闭包的形成久依赖作用域链。

解释一下作用域链–>当访问一个变量时,代码解释器会首先在当前的作用域查找,如果没找到,就去父级作用域去查找,直到找到该变量或者不存在父级作用域中,这样的链路就是作用域链,老规矩上代码:

//需要注意,每一个子函数都会拷贝上级的作用域,形成一个作用域链:
var a = 1;
function fun1() {
  var a = 2
  function fun2() {
    var a = 3;
      console.log(a);//3
		}
	}
}

以上代码,fun1 函数的作用域指向全局作用域(window)和它自己本身;fun2 函数的作用域指向全局作用域 (window)、fun1 和它本身;而作用域是从最底层向上找,直到找到全局作用域 window 为止,如果全局还没有的话就会报错。这就很形象地说明了什么是作用域链,即当前函数一般都会存在上层函数的作用域的引用,那么他们就形成了一条作用域链。

下面来解释一下闭包:闭包就是嵌套函数,它可以在内层函数中访问外层函数的作用域,它可以实现变量私有化,延长变量声明周期,适合模块化开发,上代码看一下:

function fun1() {
 var a = 1;
 return function(){
 	console.log(a);
 };
}
var result = fun1();
result();  // 1

这段代码在控制台中输出的结果是 1(即 a 的值)。可以发现,a 变量作为一个 fun1 函数的内部变量,正常情况下作为函数内的局部变量,是无法被外部访问到的。但是通过闭包,最后可以拿到 a 变量的值。

从直观上来看,闭包这个概念为 JavaScript 中访问函数内变量提供了途径和便利。这样做的好处很多,比如,可以利用闭包实现缓存,另外开发中防抖节流以及函数柯里化都是闭包的应用场景。

说到防抖和节流柯里化,就多说一嘴
防抖:单位时间内频繁触发,只执行最后一次,代码思路是利用定时器,每次触发先清掉以前的定时器

// 举例:输入框输入值,值改变调用接口,这样的话会多次调用接口,造成资源浪费
/**
* @description: 防抖函数
* @param {Function} fn 实际要执行的函数
* @param {Number} delay 延迟时间 (单位:毫秒)
* @return {Function}闭包函数
*/
function debounce(fn, delay = 500) {
	let timer = null
	console.log(this)
	return function (...args) {
		timer && clearTimeout(timer)
		timer = setTimeout(function () {
			fn.apply(this, args)
		}, delay)
	}
}
function handleInputValue(e) {
	console.log(`-----${e.target.value}`)
}
let input = document.getElementById('input')
input.addEventListener("input",debounce(handleInputValue,1000))

节流:单位时间内频繁触发,只执行一次,代码思路也是利用定时器,等定时器执行完成以后,才开启新的定时器。

/**
* @description: 节流函数
* @param {Function} fn 实际要执行的函数
* @param {Number} delay 延迟时间 (单位:毫秒)
* @return {Function}闭包函数
*/
function throttle(fn, delay = 500) {
  let timer = null
  return function (...args) {
    if (timer) {
      return false
    }
    timer = setTimeout(function () {
      fn.apply(this, args)
      timer = null //注意这里要清空,才能发起新的定时器
    }, delay)
  }
}

函数柯里化: 是指将使用多个参数的函数转换成一系列使用一个参数的函数的技术(又称为部分求值)

function currying(fn, ...params) {
  let allLength = fn.length
  let paramsArr = [...params]
  return function (...args) {
    paramsArr=[...paramsArr,...args]
    if (paramsArr.length >= allLength) {
      let arrArg=paramsArr
      return fn(...paramsArr)
    }else{
      return currying.call(this,fn,...paramsArr);
    }
  }
}
let add = (a, b, c) => a + b + c
let a = currying(add,1)
console.log(a(2,3)) //6

//解析下这段代码:

//1.定义currying函数:这个函数接受一个待柯里化的函数fn和任意数量的初始参数...params。
//2.计算fn的参数总数:通过fn.length获取fn函数期望的参数总数,并存储在allLength变量中。
//3.初始化参数数组:将传入的初始参数...params复制到一个新数组paramsArr中,以便后续操作。
//4.返回一个新的函数:这个新函数接受任意数量的参数...args。
//a. 合并参数:将新接收到的参数...args与之前的参数paramsArr合并。

//b. 检查参数数量:如果合并后的参数数量(paramsArr.length)达到了原函数fn所需的参数总数(allLength),则执行原函数fn,并将所有参数作为参数传递给fn,然后返回fn的执行结果。

//c. 递归柯里化:如果参数数量不足,则通过currying.call(this, fn, ...paramsArr)递归调用currying函数,此时this在严格模式下会是undefined(在非严格模式下是全局对象,但在这个例子中并不重要),fn和当前累积的参数...paramsArr被传递。注意这里实际上没有必要使用call,因为currying不需要特定的this上下文,直接使用currying(fn, ...paramsArr)会更简洁明了。
//闭包它可以在内层函数中访问外层函数的作用域,因为容易被外层所使用,所以使的变量无法被垃圾回收机制正常回收,从而停留在堆内存中,容易造成内存泄露,我们尽量在不需要是及时释放,设为null就行,关于闭包还有很多知识点后续继续更新。

//5.测试代码:
//定义一个接受三个参数的函数add,用于计算三个数的和。
//使用currying函数对add进行柯里化,并传入第一个参数1,得到一个新的函数a。
//调用a(2, 3),此时a函数内部的paramsArr将包含[1, 2, 3],参数数量达到add函数的要求,因此执行add(1, 2, 3)并返回结果6。

闭包的优点
1.变量被保存起来没有被销毁,随时都可以被调用

2.私有化变量,在私有化变量的基础上,保持数据缓存

缺点:
函数执行完成之后,因为闭包函数还保存着对变量的引用,导致变量没有被销毁,会非常消耗内存,有可能会导致内存泄漏(对象一直无法释放,需要手动设置为null来销毁对象)

这里提到了垃圾回收机制,后期有时间简单讲解一下浏览器的回收策略以及V8做出的优化。

JavaScript的作用域规则是理解JavaScript编程的基础之一。了解全局作用域、局部作用域(包括函数作用域和块级作用域)、变量提升以及闭包等概念,对于编写高效、可维护的JavaScript代码至关重要。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值