JavaScript中的闭包与作用域

简介: JavaScript中的闭包与作用域

js中的闭包与作用域

作用域

全局作用域只有一个,每个函数又都有作用域(环境)。

  • 编译器运行时会将变量定义在所在作用域
  • 使用变量时会从当前作用域开始向上查找变量
  • 作用域就像攀亲亲一样,晚辈总是可以向上辈要些东西

使用规范

作用域链只向上查找,找到全局 window 即终止,应该尽量不要在全局作用域中添加变量。

函数被执行后其环境变量将从内存中删除。下面函数在每次执行后将删除函数内部的 total 变量。

同时,需要注意的是,每次调用函数时都会重新开辟内存空间。

function count() {
  let total = 0;
}
count();

延伸函数环境生命周期

在函数体中,声明了变量。在函数调用后会进行垃圾清理。并且每次调用函数都会重新开辟新的内存空间。

所以每次的变量n都不是同一个变量n。

function outer(){
  let n = 1;
  function inner(){
    console.log(++n);
  }
  inner();
}

outer();//2
outer();//2

在js的垃圾清理中,如果引用类型没有被引用,就会被清理掉。

在函数体中我们有一个函数,如果对他进行引用,那么就不会被进行垃圾清理,同时,因为在引用的函数中,使用了n。

所以变量n也不会被清理。

所以反复调用a(),可以发现n的值在改变。

function outer(){
  let n = 1;
  return function inner(){
    console.log(++n);
  }
}

let a = outer();//这里outer的返回值是inner函数 所以相当于a引用了inner函数,outer内部的环境变量不会被清除
a();//2
a();//3
let b = outer();
b();//2
b();//3

对于函数的调用,我们要知道的是:

  • f()执行f函数,返回子函数
  • f()()执行子函数,返回孙函数
  • f()()()执行孙函数,返回重孙函数

注意,如果想这样执行,函数结构必须是这样:f的函数体里要return 子函数,子函数里要return 孙函数,如果没有return关键字,是不能这样连续执行的,会报错的。

function outer(){
  let n = 1;
  return function inner(){
    let m = 1;
    return function inin(){
      console.log('m:' + ++m,'n:' + ++n);
    }
  }
}

let a = outer()();//这里outer的返回值是inin函数 所以相对于a = inin
a();//m:2 n:2
a();//m:3 n:3

构造函数中作用域的使用形态

构造函数也是很好的环境例子,子函数被外部使用父级环境将被保留

function User(){
  let n = 1;
  this.show = function(){
    console.log(++n);
  }
}

let user = new User();
user.show();//2
user.show();//3

上面的例子,我们可以把他看成下面的代码

function User(){
  let n = 1;
  let show = function(){
    console.log(++n);
  };
  return {
    show: show
  };
}

let user = new User();
user.show();//2
user.show();//3

let/const

使用 let/const 可以将变量声明在块作用域中(放在新的环境中,而不是全局中)

{
    let a = 9;
}
console.log(a); //ReferenceError: a is not defined
if (true) {
    var i = 1;
}
console.log(i);//1

var/let 在for循环中的执行原理

let有块作用域。同时,要注意的是在每次执行循环体之前,JS 引擎会把 i 在循环体的上下文中重新声明及初始化一次。

for(let i=0;i<3;i++){}
拆分开来
{let i=0;}
{let i=1;}
{let i=2;}

for循环中每一次的let和函数都在块作用域中,i会直接在块作用域中找到当前循环的变量i的值。

而var没有块作用域,for循环中每次i都在全局变量中找到i,在执行setTimeout()函数时,for循环已经结束了,i为4。

所以使用var声明时,会打印4。

for (let i = 1; i <= 3; i++){
  setTimeout(function() {
    console.log(i);
  }, 1000);
}
//输出
//1
//2
//3

for (var i = 1; i <= 3; i++){
  setTimeout(function() {
    console.log(i);
  }, 1000);
}
//输出
//4
//4
//4

模拟出var的伪块级作用域

var没有块级作用域,但是有函数作用域。所以我们可以利用立即执行函数来模拟出var的伪块级作用域。‘

对于立即执行函数,要知道的是:

  • 该函数表达式在定义时就会调用
  • 该函数将拥有自己独立的词法作用域,外部无法访问
  • 该函数可以访问到外部的变量
for (var i = 1; i <= 3; i++){
  (function(a){
    setTimeout(function() {
      console.log(a);
    }, 1000);
  })(i);
}
//输出
//1
//2
//3

多级作用域嵌套

//for中用let声明变量
let arr = [];
for (let i=1;i<=3;i++){
  arr.push(function(){
    return i;
  })
}
console.log(arr.length);//3
console.log(arr[0]());//1
console.log(arr[1]());//2
console.log(arr[2]());//3

//for中用var声明变量
let arr = [];
for (var i=1;i<=3;i++){
  arr.push(function(){
    return i;
  })
}
console.log(arr.length);//3
console.log(arr[0]());//4
console.log(arr[1]());//4
console.log(arr[2]());//4

同样的,要想使用var来模拟块作用域,可以用立即执行函数来包裹。

let arr = [];
for (var i=1;i<=3;i++){
  (function(a){
      arr.push(function(){
        return a;
    });
  })(i);
}
console.log(arr.length);//3
console.log(arr[0]());//1
console.log(arr[1]());//2
console.log(arr[2]());//3

闭包使用

基本示例

闭包指子函数可以访问外部作用域变量的函数特性,即使在子函数作用域外也可以访问。如果没有闭包那么在处理事件绑定,异步请求时都会变得困难。

  • JS 中的所有函数都是闭包
  • 闭包一般在子函数本身作用域以外执行,即延伸作用域

如果我们要对数组的区间进行筛选,可以用filter方法,但是如果要返回多次,没办法进行一个复用。

let  arr = [1,2,3,4,5,6,7,8,9];

console.log(arr.filter( item => {
  return item>2 && item<7;
} ));

console.log(arr.filter( item => {
  return item>4 && item<9;
} ));

我们可以用一个函数对filter方法进行封装,其中我们就用到了闭包的特性

let  arr = [1,2,3,4,5,6,7,8,9];

function between(a,b){
  return item => {
    return item>a && item<b; //该函数作用域能访问到a,b  item也能访问到filter的item
  }
}

console.log(arr.filter(between(2,8)));//[ 3, 4, 5, 6, 7 ]
console.log(arr.filter(between(5,9)));//[ 3, 4, 5, 6, 7 ]

移动动画

在不使用闭包的情况下,如果进行多次点击,动画会出现抖动,因为每次执行函数都会重新开辟内存空间,left会被重复声明。

<style>
  button{
    position: absolute;
  }
</style>
<body>
  <button message="btn1">按钮1</button>
  <!-- <button message="btn2">按钮2</button> -->
  <script>
    let btn = document.querySelectorAll('button');
    btn.forEach(element => {
      element.addEventListener('click',()=>{
        let left = 1;
        setInterval(() => {
          element.style.left = left++ +'px';
        }, 100);
      })
    });
  </script>
</body>

如果只是不想让画面抖动,我们可以将left的定义放在他的父级作用域中

<style>
  button{
    position: absolute;
  }
</style>
<body>
  <button message="btn1">按钮1</button>
  <!-- <button message="btn2">按钮2</button> -->
  <script>
    let btn = document.querySelectorAll('button');
    btn.forEach(element => {
      let left = 1;//放在父级作用域中,执行函数时left不会被重复声明
      element.addEventListener('click',()=>{
        setInterval(() => {
          element.style.left = left++ +'px';
        }, 100);
      })
    });
  </script>
</body>

但是你会发现多次点击会造成移动速度变快。

这是因为每次执行事件监听函数都会开辟新的内存空间,每次都会有setInterval定时器执行。

我们可以让每次开辟的内存空间不执行定时器。

<style>
  button{
    position: absolute;
  }
</style>
<body>
  <button message="btn1">按钮1</button>
  <!-- <button message="btn2">按钮2</button> -->
  <script>
    let btn = document.querySelectorAll('button');
    btn.forEach(element => {
      let bind = false;
      let left = 1;
      element.addEventListener('click',()=>{
        if(!bind){
          bind = setInterval(() => {
            element.style.left = left++ +'px';
          }, 100);
        }
      })
    });
  </script>
</body>

利用闭包进行购物车的排序

let car = [{
  name: 'ipone',
  price: 8888,
  num: 3
},{
  name: 'imac',
  price: 12000,
  num:1
},{
  name: 'ipad',
  price: 6000,
  num:2
}];
function btterSort(way,type='asc'){
  return (a,b) => {
    return type === 'asc' ? a[way] - b[way] : b[way] - a[way]; 
  }
}
let [...priceWay] = car.sort(btterSort('price'))
console.log(priceWay);
//[
//   { name: 'ipad', price: 6000, num: 2 },
//   { name: 'ipone', price: 8888, num: 3 },
//   { name: 'imac', price: 12000, num: 1 }
// ]
let [...numWay] = car.sort(btterSort('num','desc'))
console.log(numWay);
//[
//   { name: 'ipone', price: 8888, num: 3 },
//   { name: 'ipad', price: 6000, num: 2 },
//   { name: 'imac', price: 12000, num: 1 }
// ]

闭包问题

内存泄漏

闭包特性中上级作用域会为函数保存数据,从而造成的如下所示的内存泄漏问题

<body>
  <div dd="houdunren">在线学习</div>
  <div dd="hdcms">开源产品</div>
</body>
<script>
  let divs = document.querySelectorAll('div');
  divs.forEach(item => {
    item.addEventListener('click',()=>{
      console.log(item.getAttribute('dd'));
      console.log(item);
    })
  })
</script>

上面的例子中,我们也许只想取到节点元素中的 dd的属性。

但是由于闭包的特性,会一直保留整个节点元素。这会对内存一定程度的造成浪费。

下面通过清除不需要的数据解决内存泄漏问题

<body>
  <div dd="houdunren">在线学习</div>
  <div dd="hdcms">开源产品</div>
</body>
<script>
  let divs = document.querySelectorAll('div');
  divs.forEach(item => {
    let dd = item.getAttribute('dd');
    item.addEventListener('click',()=>{
      console.log(dd);
      console.log(item);//null
    })
    item = null;
  })
</script>

可以看到,让item = null 从而让节点元素没有被引用,所以就会被进行垃圾回收了。

为什么item = null;在后面,前面函数中的item会为null,而不是执行了一次后再为null?

因为在这里只是绑定dom事件(这里,只是绑定 item 的点击事件的回调函数,只是绑定一下,一瞬间的事,不需要阻塞),而不是直接执行。

闭包中this的历史遗留问题

this 总是指向调用该函数的对象,即函数在搜索 this 时只会搜索到当前活动对象。

下面是函数因为是在全局环境下调用的,所以 this 指向 window,这不是我们想要的。

其中func()的返回值为function(){return this;}所以使用func()()来调用方法里面的函数

注意这并不是由obj对象来调用,而是window全局对象。

在对象方法的普通函数中,使用this,这时的对象的引用不是他的上层方法的对象,而是全局对象window

<body>

</body>
<script>
  let obj = {
    site: 'dd',
    func(){
      return function(){
        return this;
      };
    }
  };
  console.log(obj.func()());//window对象
</script>

使用箭头函数解决这个问题

<body>

</body>
<script>
  let obj = {
    site: 'dd',
    func(){
      return ()=>{
        return this.site;
      };
    }
  };
  console.log(obj.func()());//dd 指向obj这个对象
</script>
相关文章
|
3月前
|
存储 JavaScript 前端开发
|
8月前
|
JavaScript 前端开发
js的作用域作用域链
【10月更文挑战第29天】理解JavaScript的作用域和作用域链对于正确理解变量的访问和生命周期、避免变量命名冲突以及编写高质量的JavaScript代码都具有重要意义。在实际开发中,需要合理地利用作用域和作用域链来组织代码结构,提高代码的可读性和可维护性。
|
5月前
|
前端开发 JavaScript Java
JavaScript闭包深入剖析:性能剖析与优化技巧
JavaScript 闭包是强大而灵活的特性,广泛应用于数据封装、函数柯里化和事件处理等场景。闭包通过保存外部作用域的变量,实现了私有变量和方法的创建,提升了代码的安全性和可维护性。然而,闭包也可能带来性能问题,如内存泄漏和执行效率下降。为优化闭包性能,建议采取以下策略:及时解除对不再使用的闭包变量的引用,减少闭包的创建次数,使用 WeakMap 管理弱引用,以及优化闭包结构以减少作用域链查找的开销。在实际开发中,无论是 Web 前端还是 Node.js 后端,这些优化措施都能显著提升程序的性能和稳定性。
170 70
|
9月前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript中的闭包:原理与实战
【10月更文挑战第12天】深入理解JavaScript中的闭包:原理与实战
|
5月前
|
自然语言处理 JavaScript 前端开发
当面试官再问我JS闭包时,我能答出来的都在这里了。
闭包(Closure)是前端面试中的高频考点,广泛应用于函数式编程中。它不仅指函数内部定义的函数,还涉及内存管理、作用域链和垃圾回收机制。闭包可以让函数访问其外部作用域的变量,但也可能引发内存泄漏等问题。通过合理使用闭包,可以实现模块化、高阶函数和回调函数等应用场景。然而,滥用闭包可能导致代码复杂度增加、调试困难以及潜在的性能问题。为了避免这些问题,开发时应谨慎处理闭包,避免不必要的嵌套,并及时清理不再使用的变量和监听器。
197 16
当面试官再问我JS闭包时,我能答出来的都在这里了。
|
4月前
|
缓存 自然语言处理 JavaScript
JavaScript中闭包详解+举例,闭包的各种实践场景:高级技巧与实用指南
闭包是JavaScript中不可或缺的部分,它不仅可以增强代码的可维护性,还能在模块化、回调处理等场景中发挥巨大作用。然而,闭包的强大也意味着需要谨慎使用,避免潜在的性能问题和内存泄漏。通过对闭包原理的深入理解以及在实际项目中的灵活应用,你将能够更加高效地编写出简洁且功能强大的代码。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
8月前
|
JavaScript 前端开发
js 闭包的优点和缺点
【10月更文挑战第27天】JavaScript闭包是一把双刃剑,在合理使用的情况下,它可以带来很多好处,如实现数据封装、记忆功能和模块化等;但如果不注意其缺点,如内存泄漏、变量共享和性能开销等问题,可能会导致代码出现难以调试的错误和性能问题。因此,在使用闭包时,需要谨慎权衡其优缺点,根据具体的应用场景合理地运用闭包。
227 58
|
8月前
|
前端开发 JavaScript 数据处理
CSS 变量的作用域和 JavaScript 变量的作用域有什么不同?
【10月更文挑战第28天】CSS变量和JavaScript变量虽然都有各自的作用域概念,但由于它们所属的语言和应用场景不同,其作用域的定义、范围、覆盖规则以及与其他语言特性的交互方式等方面都存在明显的差异。理解这些差异有助于更好地在Web开发中分别运用它们来实现预期的页面效果和功能逻辑。
141 11
|
8月前
|
JavaScript 前端开发
javascript的作用域
【10月更文挑战第19天javascript的作用域
|
8月前
|
JavaScript 前端开发
如何在 JavaScript 中实现块级作用域?
【10月更文挑战第29天】通过使用 `let`、`const` 关键字、立即执行函数表达式以及模块模式等方法,可以在JavaScript中有效地实现块级作用域,更好地控制变量的生命周期和访问权限,提高代码的可维护性和可读性。

热门文章

最新文章