「JavaScript基础整理」关于函数、作用域你了解 多少?
哈喽,大家好,延续之前,继续把相关的基础知识点整理下去,本期主要以理论为主,相关的案例会少一些,也希望大家能给出建议,一起互勉。
一、函数的基本认识
为什么需要用到函数呢,比如我们上期所实现的数组的各个方法,这些代码是可能被大量使用的,为了避免代码的冗余,我们将其封装成一个个的方法,在使用的时候直接调用方法名,传入参数即可,极大的方便了我们的编码。
1.1 函数的使用
函数的使用分为两步:声明 ————> 调用
1.1.1 声明函数
function 函数名() {
// 函数体
}
function handleData() {
console.log('处理数据的函数体')
}
function 关键字 用来声明一个函数 function 和 var 一样,它们声明的函数和变量都在 JavaScript 预编译时被解析,也被称为函数提升和变量提升。 函数声明完成后,不调用,不执行函数体
1.1.2 调用函数
函数名()
handleData() // 处理数据的函数体
1.2 函数的参数
- 在声明函数的时候,在函数名之后是一个由一个小括号包含的参数列表,参数之间以逗号分隔,这些参数被称为形参;
- 在函数被调用的时候,传递参数,这些参数被称为实参。
注意:
参数仅能够在函数体内被访问,参数是局部(函数)作用域的私有成员 如果想使用局部作用域的变量,我们可以将其抛出去,闭包也可以等;
1.3 函数的几种形式
1.3.1 声明函数
声明一个名称为name的函数
function name() {
// 函数体
}
1.3.2 匿名函数
没有函数名,直接声明函数体及参数
function () {
// 函数体
}
1.3.3 嵌套函数
函数嵌套函数
function name(a, b) {
function age() {
return '我是内层函数'
}
return console.log(age() + '我是外层函数')
}
1.4 augments的使用
augments是函数的一个内置对象,是函数的实参集合,仅能够在函数体内可见,并可以直接访问。arguments 对象能够增强函数应用的灵活性,当我们不确定函数需要传入多少个参数的时候,可以不定义参数,在调用的时候直接根据arguments来获取实参。
arguments的length 属性和 callee 属性
- length属性可以获取实参的个数
- callee属性可以获取函数本身,可以实现递归的功能
- callee.length属性可以获取函数形参的个数 通过和arguments.length属性比较形参和实参的个数
// callee 递归使用
let count = 1
function name() {
if(count < 5) {
console.log(count)
count++
arguments.callee()
}
return
}
name()
二、作用域
作用域表示变量的作用范围、可见区域,包括词法作用域和执行作用域。作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。JavaScript采用的是词法作用域,也称之为静态作用域。
- 静态作用域: 根据代码的结构关系来确定作用域
- 动态作用域: 在代码执行的时候确定作用域 为什么说JavaScript采用的是静态作用域呢,我们来分析一个案例:
let value = '全局变量';
function test() {
console.log(value);
}
function useTest() {
let value = '函数useTest局部变量';
test();
}
useTest(); //全局变量
案例分析
静态分析: 执行 useTest 函数从而调用test函数,先从 test 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于 全局变量,所以结果会打印 全局变量
动态分析: useTest 函数从而调用 test 函数,依然是从 test 函数内部查找是否有局部变量 value。如果没有,就从调用函数 useTest 的作用域中查找 value 变量,所以结果会打印 函数useTest局部变量 最后打印的是 全局变量, 由此可见,JavaScript采用的是静态作用域
2.1 全局变量和局部变量
- 全局变量:在全局作用域下的变量(注意:在函数内部中,如果没有声明,直接赋值的变量也属于全局变量)
- 局部变量:在局部作用域下的变量比如函数中声明的变量,且只能咋局部使用。
2.2 全局变量和局部变量的区别
- 全局变量:在全局使用,没有限制,只有在浏览器关闭的时候才销毁
- 局部变量:只在函数内部使用,代码块执行完成就销毁。
2.3 可执行代码类型
- 全局代码: 代码加载时首先进入的环境,但不包括任何 function 体内的代码。
- 函数代码: 是指 function 被解析的源代码,只能解析最外层的函数代码,函数内层的嵌套函数除外。
- eval代码: 传递给 eval 内置函数的代码,不建议使用eval,能不用,绝对不用。
在执行可执行代码的时候,每执行一段可执行代码,都会创建执行的上下文,可执行上下文包含三个部分: 变量对象、作用域链和 this。
对于执行上下文、词法环境等概念的理解,我们可以参考ES5/可执行代码与执行环境。我们这里仅做一个盲点的普及。
2.4 执行上下文 与 作用域 的关系
2.4.1 二者之间的联系
- 上下文环境是 从属于 所在作用域的
- 全局上下文环境 从属于 全局作用域
- 函数上下文环境 从属于 函数作用域
2.4.2 二者之间的区别
- 全局作用域之外,每个函数在定义的时候都会创建自己的作用域;全局执行上下文环境,是在全局作用域确定之后,JS代码马上执行之前创建的;函数执行上下文环境是在函数调用的时候,内部代码执行之前被创建出来的;
- 作用域属于静态的,在定义完成后就被创建出来,一直不会发生改变;执行上下文环境是属于动态的,每次在函数调用时创建,调用完成后自动销毁。注意:特殊情况下,不会被销毁!
2.4.3 案例分析
案例一: 简单作用域变量检索
var num = 100;
function test() {
console.log(num)
}
function testA(f) {
var num = 200
f()
}
testA(test) // 100 作用域一旦确定不会更改
案例二: 一般作用域 函数 检索
function test() {
console.log(test)
}
let objB = {
testB: function() {
console.log('寻找test------', test)
console.log('寻找testB------', this.testB)
}
}
let objA = {
testA: function() {
console.log('寻找test------', test)
console.log('寻找testA------', testA)
}
}
test()
objB.testB()
objA.testA()
我们先来看一下打印的结果和你猜测的是否一致:
** 现在让我们来一起分析一下这个过程把 **
- test() 调用后 会在自身的作用域中寻找有无test 属性 没有则去 全局作用域中寻找
- testB() 调用后 会在自身的作用域中寻找 test 以及 objB.testB 属性 没有则去 全局作用域中寻找
- testA() 调用后 会在自身的作用域中寻找 test 以及 testA 属性 没有则去 全局作用域中寻找
在这三个函数中均没有test属性,也没有 testA、objB.testB属性,所以均会在全局作用域中寻找,而全局作用域中拥有test属性以及bjB.testB属性,没有testA属性,所以在调用objA.testA()的时候会报错。
2.5 作用域链
作用域链是 JavaScript 提供的一套解决标识符的访问机制,通俗点来说就是内部函数访问外部函数的变量,采用的是链式查找的方式。
三、闭包--(嵌套函数)
闭包是由函数以及声明该函数的词法环境组合而成的。该环境包含了这个闭包创建时作用域内的任何局部变量。说白了,闭包就是一个能够持续存在的函数上下文活动对象。这里,我们简单的举个例子说明一下:
function test() {
let num = 10;
return function() {
console.log(num)
}
}
let myNum = test()
myNum()
按照正常的函数执行来说,函数被调用的时候,产生一个临时上下文活动对象,num,function等作为私有变量都可以作为对象的属性在局部使用,使用完成,这个临时的上下文活动对象会被立即释放,避免占用资源。 但是当该函数处于活跃状态时,函数外部环境引用了某些属性,那么该上下文活动对象就会继续存在,直到该引用被销毁。在该例子中,test函数返回的内部函数被外部环境的 myNum 引用,导致其自身无法被销毁。在这时候,我们在使用内部函数的时候,可以发现num值依旧存在,即myNum()打印结果为10
3.1 闭包的形成方式
- 内部函数被外部引用: 上边的例子中便是该方式的具体体现,这里不再列举说明
- 内部函数对外部开放: 其实该方式和上一种方式原理相同,只是产生的方式不同,上边的方式是抛出内部函数,外部坏境变量承接引用,该方式则是创建上下文活动对象的时候就将内部函数赋值给外部环境变量。让我们看一下实现方式:
let outDot
function test() {
let num = 10;
outDot = function() {
console.log(num)
}
}
console.log(outDot) //undefined
test()
console.log(outDot) // ƒ () { console.log(num) }
outDot() // 10
当然,除了外部环境引用内部函数可以生成闭包之外,引用数组或者对象也可以生成闭包,大家有兴趣可以尝试实现一下。
3.2 闭包的使用
这里我们使用一个例子说明:
let test = (function () {
let testArr = []
return function (x) {
testArr.push(x)
return testArr
}
})()
let testA = test('testA传入的值')
console.log(testA)
let testB = test('testB传入的值')
console.log(testA)
console.log(testB)
从打印的结果中我们不难看出,testArr的值是处于累加的状态的,并没有在两次调用的时候清空,这种使用有利有弊,如果我们不注意使用规范,很容易造成数据错乱的情况,而且在函数被调用之后,并没有释放属性,占用大量的内存资源,而且容易造成内存泄漏。
四、函数的节流与防抖
在我们的工作中,一些浏览器事件:window.onresize/window.mousemove等,触发的频率非常高,就会造成浏览器性能的问题,比如:向后台发送请求,频繁触发,对服务器造成不必要的压力。为了解决这些问题,提升用户体验,我们可以采用函数节流或者防抖。下面我们就来实现一下:
4.1 函数的节流
窗口调整(resize)、页面滚动(scroll)、DOM元素拖拽功能实现(onmousemove)、抢购疯狂点击(mousedown)等场景下,函数需要频繁触发时:函数执行一次后,只有大于设定的执行周期后才会执行第二次,适合多次事件按时间做平均分配触发
实现思路:
- 首先先我们需要明确,第二次调用的时间和上一次调用的时间间隔大于规定的时间段即可调用
- 其次 我们需要确保第一次调用能立即执行,也就是第一次调用时间设置为0即可
- 每次调用时 获取当前时间戳 和 上次调用的时间做比较
- 每次调用可执行的时候将 当前时间存储起来方便下一次比较
<button id="throttle">节流</button>
<script>
function throttle(callback, time) {
let start = 0
return function (...args) {
const current = Date.now()
if (current - start > time) {
start = current
callback.apply(this, args)
}
}
}
function handleClick (name) {
console.log('事件处理函数', name)
}
document.getElementById('throttle').onclick = throttle(handleClick, 2000)
</script>
4.2 函数的防抖
将即将被执行的函数用setTimeout延迟一段时间执,在规定的时间内,只让最后一次生效,前面的不生效,适合多次事件一次响应的情况,如果在规定的时间内多次触发,以最后一次触发的时间计时为主,到达时间,触发回调。
实现思路:
- 首先明确 每次点击 如果没有到达时间段,则需要清空上次定时器,重新计时
- 其次 满足时间段 执行回调前,将定时器标记删除
<button id="debounce">防抖</button>
<script>
function debounce(callback, time) {
return function (...args) {
console.log('事件防抖')
if(callback.timeoutId) {
clearTimeout(callback.timeoutId)
}
callback.timeoutId = setTimeout(() => {
delete callback.timeoutId
callback.apply(this, args)
}, time)
}
}
function handleClick () {
console.log('事件处理函数', this)
}
document.getElementById('debounce').onclick = debounce(handleClick, 2000)
</script>
五、 JS的惰性载入函数
惰性载入表示函数执行的分支只会在函数第一次掉用的时候执行,在第一次调用过程中,该函数会被覆盖为另一个按照合适方式执行的函数,这样任何对原函数的调用就不用再经过执行的分支了。主要应用在浏览器兼容等问题上,我们也可以应用在权限控制上,只需要判断一次,后续不需要在进行二次判断:
实例分析
- 调用 addEvent 函数的时候,对浏览器所支持的能力进行检查
- 首先检查是否支持 addEventListener 方法;
- 如果不支持,再检查是否支持 attachEvent 方法;
- 如果还不支持,就用 dom 0 级的方法添加事件。 这种载入方法的原理就是,当我们在浏览器载入的时候就判断该方法具体采用那种形式,一旦确认就不再改变,取缔了之前每调用一次方法就判断一次兼容性的步骤,从而提高了性能
var addEvent = (function () {
if (document.addEventListener) {
return function (type, element, fun) {
element.addEventListener(type, fun, false);
}
}
else if (document.attachEvent) {
return function (type, element, fun) {
element.attachEvent('on' + type, fun);
}
}
else {
return function (type, element, fun) {
element['on' + type] = fun;
}
}
})();
六、 小结
作用域比较简单,东西比较少,函数是最为复杂的一个数据类型了,里边的好多东西由于时间关系这里并没有涉及到,比如:偏函数、递归、函数柯里化等等,我们工作的时候,可能目前还接触不到这些,但是熟练使用函数中的每个概念,会极大的方便我们的开发,提升我们的开发质量,比如惰性载入函数的使用会提升我们的应用性能等等。总之还有很多东西需要我们去了解熟悉,后期如果有时间,阿树会继续整理出来一起研究。本期结束,我是阿树,我们下期再见!
七、声明
- 创作不易,转载请注明来源
- 创作不易,copy请留情
八、参考文献
转载自:https://juejin.cn/post/6895653834673094669