JS基础(一)

123 阅读8分钟

之前记在word里的学习笔记太乱了,整理到掘金上来复习一遍。

  • 执行上下文
  • 作用域
  • 闭包

执行上下文

执行上下文也就是Javascript代码的执行环境。

执行上下文的类型分有三种:

  • 全局执行上下文:只有一个,浏览器中的全局对象就是 window对象,this 指向这个全局对象
  • 函数执行上下文:存在无数个,只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文。(会创建一个私有作用域,函数内部声明的任何变量都不能在当前函数作用域外部直接访问)
  • Eval 函数执行上下文:指的是运行在 eval 函数中的代码,很少用而且不建议使用

生命周期 创建阶段 → 执行阶段 → 回收阶段

  • 创建阶段:函数环境会创建变量对象:arguments对象(并赋值)、函数声明(并赋值)、变量声明(不赋值),函数表达式声明(不赋值);会确定this指向;会确定作用域
  • 执行阶段:变量赋值、函数表达式赋值,使变量对象变成活跃对象

执行栈 执行栈,也叫调用栈,具有栈的特点( LIFO后进先出),用于存储在代码执行期间创建的所有执行上下文。

  • 当Javascript引擎开始执行第一行脚本代码的时候,它就会创建一个全局执行上下文然后将它压到执行栈中
  • 每当引擎碰到一个函数的时候,它就会创建一个函数执行上下文,然后将这个执行上下文压到执行栈中。
  • 引擎会执行位于执行栈栈顶的执行上下文(一般是函数执行上下文),当该函数执行结束后,对应的执行上下文就会被弹出,然后控制流程到达执行栈的下一个执行上下文
  • 栈底永远是全局环境的执行上下文,栈顶永远是正在执行函数的执行上下文
  • 只有浏览器关闭的时候全局执行上下文才会弹出

作用域

  • 作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期
  • 代码执行过程中会创建变量对象的一个作用域链,作用域链的前端是当前执行环境的变量对象,末端是全局执行环境的变量对象。
  • 当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链
  • 作用域链的作用是保证执行环境里有权访问的变量和函数是有序的,作用域链的变量只能向上访问,变量访问到window对象即被终止,作用域链向下访问变量是不被允许的

作用域链针对函数作用域来说的,比如创建了一个函数,函数里面又包含了一个函数,那么就会有全局作用域、函数1的作用域、函数2的作用域。

闭包

闭包就是能够读取其他函数内部变量的函数,其实就是利用了作用域链向上查找的特点。

闭包让你可以在一个内层函数中访问到其外层函数的作用域。每当创建一个函数,闭包就会在函数创建的同时被创建出来,作为函数内部与外部连接起来的一座桥梁。

写一个简单的闭包 创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量,利用闭包可以突破作用链域。

function fun() {
  var a = 10;  //a是被fun函数创建的局部变量
  return function () {  //内部函数,一个闭包
    console.log(a);  //使用父函数中声明的变量
  };
}
var a = 20;
var f = fun();
f();  //10

内部函数没有自己的局部变量。由于闭包的特性,它可以访问到外部函数的变量。

注意闭包利用作用域链向上查找的特点,这里查找的是包裹它的父级中声明的a,不是全局作用域中的a。依据的是函数定义时的作用域链,而不是函数执行时。

作用

  • 内部函数可以引用外层的参数和变量
  • 让这些变量始终保持在内存中(闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在的词法环境依然存在,以达到延长变量的生命周期的目的。)
  • 封装对象的私有属性和私有方法

由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

举例说明闭包使函数内部的变量无法进行销毁,始终保持在内存中

function myMethod(){
      var num=6;
      return function(){
            var n=0;
            console.log(++n);
            console.log(++num);
      }
}
myMethod();

var myFn=myMethod();//
myFn();//n=1,num=7    
myFn();//n=1,num=8   

myMethod()执行后,返回的是个方法(就是所谓的闭包),因为这个方法中还用到了myMethod里面的变量num,会导致num无法释放。

将返回的方法用变量保存起来,执行发现num没有被销毁,仍然保存着值。

注意:无法销毁变量,使用不当,容易造成内存泄漏!

应用场景 ①for循环中使用闭包解决 var 定义的问题

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i);
  }, i * 1000);
}

因为 setTimeout 是个异步函数,会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。

使用闭包后,保留变量i

for (var i = 1; i <= 5; i++) {
  (function (j) {
    setTimeout(function timer() {
      console.log(j);
    }, j * 1000);
  })(i);
}

②函数柯里化

柯里化的目的:避免频繁调用具有相同参数函数的同时,又能够轻松的重用

// 假设有一个求长方形面积的函数
function getArea(width, height) {
    return width * height
}
// 如果碰到的长方形的宽总是10
const area1 = getArea(10, 20)
const area2 = getArea(10, 30)
const area3 = getArea(10, 40)

// 可以使用闭包柯里化这个计算面积的函数
function getArea(width) {
    return height => {
        return width * height
    }
}

const getTenWidthArea = getArea(10)
// 之后碰到宽度为10的长方形就可以这样计算面积
const area1 = getTenWidthArea(20)

// 而且如果遇到宽度偶尔变化也可以轻松复用
const getTwentyWidthArea = getArea(20)

③使用闭包模拟私有方法

私有方法只能被同一个类中的其它方法所调用。JavaScript 不提供原生的支持,但是可以使用闭包模拟私有方法。

下面使用闭包来定义公共函数,且其可以访问私有函数和变量。这也叫模块方式。

var Counter = (function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }   
})();
 
console.log(Counter.value()); //0
Counter.increment();
Counter.increment();
console.log(Counter.value()); //2
Counter.decrement();
console.log(Counter.value()); //1

这次只创建了一个环境,为三个函数所共享:Counter.incrementCounter.decrementCounter.value

该共享环境创建于一个匿名函数体内,该函数一经定义立刻执行。环境中包含两个私有项:名为 privateCounter 的变量和名为 changeBy 的函数。 这两项都无法在匿名函数外部直接访问。必须通过匿名包装器返回的三个公共函数访问。

这三个公共函数是共享同一个环境的闭包。多亏 JavaScript 的词法范围的作用域,它们都可以访问 privateCounter 变量和 changeBy 函数。

上述定义了一个匿名函数用于创建计数器,然后直接调用该函数,并将返回值赋给 Counter 变量。也可以将这个函数保存到另一个变量中,以便创建多个计数器。

var makeCounter = function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }  
};
 
var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); //0
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); //2
Counter1.decrement();
console.log(Counter1.value()); //1
console.log(Counter2.value()); //0

两个计数器 Counter1 和 Counter2 是维护它们各自的独立性的,每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境,不会影响另一个闭包中的变量。

参考链接: JavaScript—用闭包模拟私有方法

③防抖和节流

防抖:你尽管触发事件,我只在最后一次触发后的一段时间内执行。

例如搜索框,如果每输入一个字符就触发一次,服务器压力巨大。利用防抖,在输入完之后停顿某个时间后才触发。

function debounce2(fn,wait) {
    let timer = null; //创建一个标记用来存放定时器的返回值
    return () => {
        clearTimeout(timer); //清除定时器
        timer = setTimeout(() => {  //创建新的 setTimeout
            fn.apply(this, arguments);
        }, wait);
    }
}

节流:持续触发事件,但一段时间内只执行一次。

比如打开一个网页,当我们向下滚动页面的时候,onscroll事件触发的频率太高太高,稍微向下滚动一丢丢,就已经触发了很多次,而且其中很多的触发并没有实在的意义,如何减少触发的频率,减少那么多的计算操作呢?这就用到节流,让这个onscroll事件触发后周期性执行。

function throttle(func, wait) {
    var timer; //闭包保存这个变量
    return function () {
        var context = this;
        var args = arguments;
        if (!timer) {
            timer = setTimeout(function () {
                func.apply(context, args);
                timer = null; //消除
            }, wait);
        }
    }
}

setTimeout返回的标记当做判断条件:判断当前定时器是否存在,如果存在表示还在冷却,并且在执行func之后消除定时器表示激活。

④设计模式中的单例模式

var SingleTon = function(){
    var instance;
    class CreateSingleTon {
        constructor (name) {
            if(instance) return instance;
            this.name = name;
            this.getName();
            return instance = this;
        }

        getName() {
            return this.name;
        }
    }
    return CreateSingleTon;
}();

var a = new SingleTon('instance1');
console.log(a.getName()); //输出instance1
var b = new SingleTon('instance2');
console.log(b.getName()); //输出instance1
console.log(a === b); //输出true

未完待续...