详解作用域链、原型链、闭包

一、作用域

JavaScript的作用域是我们可以有效访问变量或函数的区域。JS具有三种类型的作用域:全局作用域、函数作用域和块级作用域(ES6)

全局作用域

全局空间中声明的变量或函数,我们可以在代码的任何位置访问到他们。

全局作用域在页面打开时创建,页面关闭时销毁,在全局作用域中有一个全局对象window(代表的是一个浏览器的窗口,由浏览器创建),可以直接使用。

  • 所有创建的变量都会作为window对象的属性保存

在这里插入图片描述

  • 所有创建的函数都会作为window对象的方法保存

在这里插入图片描述

函数作用域

函数内部就是函数作用域,调用函数是创建函数作用域,函数执行完毕之后,函数作用域销毁。每调用一次函数就会创建一个新的函数作用域,它们之间是相互独立的。

在函数内部声明的变量、函数和参数可以在该函数内部访问。函数外部是访问不到的。

const fn = function myFunc() {
  var a = 66;
}
fn();
console.log(a); //ReferenceError: a is not defined

块级作用域

被块{}包含的就是块级作用域。

{}中声明的变量(let const)只能在会计作用域内访问

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

补充----动态作用域this(下一篇文章会继续介绍)

二、变量的作用域

在JavaScript中,根据作用域的不同,变量可以分为两种:全局变量局部变量

全局变量

1、在全局作用域下声明的变量叫做 全局变量(在函数外部定义的变量)

2、全局变量在全局(代码的任何位置)下都可以使用;全局作用域中无法访问到局部作用域中的变量。

3、全局变量第一种创建方式:在全局作用域下 var声明的变量是全局变量

4、全局变量第二种创建方式:如果在函数内部,没有使用 var关键字声明直接赋值的变量也属于 全局变量。(不建议使用)

局部变量

1、在局部作用域下声明的变量叫做局部变量(在函数内部定义的变量)

2、局部变量只能在函数内部使用,在局部作用域中可以访问到全局变量。

3、在函数内部 var 声明的变量就是局部变量

4、函数的形参实际上就是局部变量

全局变量和局部变量的区别

全局变量:在任何一个地方都可以使用,全局变量只有在浏览器关闭的时候才会销毁,比较占用内存资源

局部变量:只能在函数内部使用,当其所在代码块被执行时,会被初始化;当代码块执行完毕就会销毁,因此更节省节约内存空间;

三、作用域链

内部函数访问外部函数的变量,采取的是链式查找的方法来决定取那个结构,这种结构称之为作用域链

作用域链的原则:就近原则

(作用域链采用链式查找的方式,一层一层向上查找,先查找外面的嵌套的函数是否有所需内容,找到就输出相应的结果,如果没有再向上查找,直到查到全局作用域,这么一个查找过程形成的链条。)

四、变量和函数的声明提升

javascript是由浏览器解释执行的脚本语言,由浏览器js解释器进行解释执行,总的过程分为两个阶段,预编译阶段执行阶段

在预编译阶段,js引擎就会读取变量的定义并确定其作用域即生效范围。

变量提升的表现是,无论我们在函数中何处位置声明的变量,好像都被提升到了函数的首部,我们可以在变量声明前访问到而不会报错。

造成变量声明提升的本质原因是 js 引擎在代码执行前有一个解析的过程,创建了执行上下文,初始化了一些代码执行时需要用到的对象。当我们访问一个变量时,我们会到当前执行上下文中的作用域链中去查找,而作用域链的首端指向的是当前执行上下文的变量对象,这个变量对象是执行上下文的一个属性,它包含了函数的形参、所有的函数和变量声明,这个对象的是在代码解析的时候创建的。这就是会出现变量声明提升的根本原因。

变量的声明提升

使用 var 关键字声明的变量,会在所有的代码执行之前被声明。(但是不会赋值)

全局变量即使是写在最下面,也相当于在所有代码之前的最上面声明的变量。


var name = 'Irene';

function say(){
  console.log(name); //输出:undefined
  var name = 'Yovela';
  console.log(name); //输出:'Yovela'
}

say();

解析:上述代码从结果看,say函数执行第一次打印name时,并未打印全局的name (Irene),而是打印局部的name(undefined),这是因为在预编译阶段,say函数内部进行了变量声明提升,提升后的执行效果如下:


var name = 'Irene';

function say(){
  var name; //变量name声明提升至作用域顶部,但未赋值,故为undefined
  console.log(name); //存在局部name,则无视全局name
  var name = 'Yovela'; //变量赋值保持原位
  console.log(name); //输出:'Yovela'
}

say();

如果声明变量的时候不使用 var 关键字,那么变量就不会被声明提前。

函数声明提升

函数的两种创建方式:

  • 函数声明function () {}

它会在所有代码执行之前就被创建。所以可以在函数声明之前被调用。

say(); //输出:'saying'

function say() {
  console.log('saying');
}
  • 函数表达式var 变量名 = function 函数名() {}

不会被声明提前,所以不能再声明前调用。

say(); //报错:say is not a function

var say = function () {
  console.log('saying');
}

解析:同样地先执行函数,后创建函数,结果却是不一样。原因在于,通过函数声明的方式,该函数声明(包括定义)会被提升至作用域的顶部,而表达式的创建方式则只提升了变量say至作用域的顶部,此时的say其值为undefined,调用say()自然报错“say is not a function”。

var say = function () {
  console.log('1');
};

function say() {
  console.log('2');
};

say(); //输出:'1'

解析:预编译阶段进行变量声明提升和函数声明提升后,上述代码执行效果等同于:

var say; //变量声明提升

function say() { //函数声明提升
  console.log('2');
}

say = function () { //变量赋值保持原位执行,say函数被覆盖
  console.log('1');
};

say(); //输出'1'

变量声明和函数声明提升的联系

  1. 函数声明提升,会将函数的声明和定义全都提升至作用域顶部。变量声明提升,只提升声明部分(未赋值状态),赋值部分保持原位置不动。
  2. 同名情况下,函数声明提升优先级要高于变量声明提升,且提升后该函数声明定义不会被提升后的同名变量声明所覆盖,但是会被后续顺序执行的同名变量赋值所覆盖。

五、原型

原型对象理解

  • 函数对象的 prototype 属性

我们创建的每一个函数都有一个 prototype 属性,这个属性是一个指针,指向一个对象。原型对象中的方法和属性都可以被函数的实例所共享。所谓的函数实例是指以函数作为构造函数创建的对象,这些对象实例都可以共享构造函数的原型的方法。

这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法,简单来说,该函数实例化的所有对象的__proto__的属性指向这个对象,它是该函数所有实例化对象的原型。

function Person(){

}

// 为原型对象添加方法
Person.prototype.sayName = function(){
    alert(this.name);
}

在这里插入图片描述

  • constructor 属性

当函数创建,prototype 属性指向一个原型对象时,在默认情况下,这个原型对象将会获得一个 constructor 属性,这个属性是一个指针,指向 prototype 所在的函数对象。

拿前面的一个例子来说 Person.prototype.constructor 就指向 Person 函数对象。

在这里插入图片描述

  • 对象的 proto 属性

当我们调用构造函数创建一个新实例后,在这个实例的内部将包含一个指针,指向构造函数的原型对象。


var student = new Person();

console.log(student.__proto__ === Person.prototype); // true

从上面我们可以看出,这个连接是存在与实例与构造函数的原型对象之间的,而不是存在于实例和构造函数之间的。

在这里插入图片描述

原型属性

  • 属性访问

每当代码读取对象的某个属性时,首先会在对象本身搜索这个属性,如果找到该属性就返回该属性的值,如果没有找到,则继续搜索该对象对应的原型对象,以此类推下去。

  • 属性判断

在属性确认存在的情况下,我们可以使用 hasOwnProperty()方法来判断一个属性是存在与实例中,还是存在于原型中。注意这个方法只有在给定属性存在于实例中时,才会返回 true 。

如果这个属性不一定存在的话,这样判断就不够准确,因此我们需要首先判断这个属性是否存在,然后再进行上面的判断操作。

判断一个属性是否存在,我们可以使用in 操作符,它会在对象能够访问给定属性时返回 true,无论该属性存在于实例还是原型中。

六、原型链

ECMAScript 中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用的一个引用类型继承另一个引用类型的属性和方法。

原型链的主要实现方法是让构造函数的 prototype 对象等于另一个类型的实例,此时的 prototype 对象因为是实例,因此将包含一个指向另一个原型的指针(proto),相应地另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与类型的链条。这就是原型链的基本概念

不包含函数原型的以上实例所包含的原型链的关系图:

在这里插入图片描述

加入了函数原型后的原型链的关系图:

在这里插入图片描述

function Super() {};
function Middle() {};
function Sub() {};
Middle.prototype = new Super();
Sub.prototype = new Middle();
var suber = new Sub();

在这里插入图片描述

七、闭包

基本含义

闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问当前函数的局部变量。

比如:

function outer()
{    
    var a = 1;    
    function inner()
    {        
        console.log(a);    
    }    
    inner(); // 1}outer();
}

在 inner 函数中,我们可以通过作用域链访问到 a 变量,因此这就可以算是构成了一个闭包,因为 a 变量是其他函数作用域中的变量。

特点

  • 让外部访问函数内部变量成为可能;
  • 能够读取函数内部的变量;
  • 局部变量会常驻在内存中;
  • 可以避免使用全局变量,防止全局变量污染;
  • 会造成内存泄漏(有一块内存空间被长期占用,而不被释放)

应用场景

  • setTimeout 传参
//原生的setTimeout传递的第一个函数不能带参数
setTimeout(function(param) {
    alert(param)
},1000);


//通过闭包可以实现传参效果
function myfunc(param) {
    return function() {
        alert(param)
    }
};
var f1 = myfunc(1);
setTimeout(f1,1000);
  • 回调
function say (value) {
    alert(value);
}
function execute (someFunction, value) {
    someFunction(value);
}
execute(say, 'hi js.');
  • IIFE(自执行函数)
  var arr = [];
    for (var i=0;i<3;i++){
      //使用IIFE
      (function (i) {
        arr[i] = function () {
          return i;
        };
      })(i);
    }
    console.log(arr[0]()) // 0
    console.log(arr[1]()) // 1
    console.log(arr[2]()) // 2
  • 函数防抖、节流

比如绑定响应鼠标移动、窗口大小调整、滚屏等事件时,绑定的函数触发的频率会很频繁。若稍处理函数微复杂,需要较多的运算执行时间和资源,往往会出现延迟,甚至导致假死或者卡顿感。为了优化性能,这时就很有必要使用 debounce防抖throttle节流了。

debounce与throttle区别

防抖 (debounce) :多次触发,只在最后一次触发时,执行目标函数。

节流(throttle):限制目标函数调用的频率,比如:1s内不能调用2次。

// 函数防抖的实现
function debounce(fn, wait) {
  var timer = null;

  return function() {
    var context = this,
      args = arguments;

    // 如果此时存在定时器的话,则取消之前的定时器重新记时
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }

    // 设置定时器,使事件间隔指定事件后执行
    timer = setTimeout(() => {
      fn.apply(context, args);
    }, wait);
  };
}

在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。

// 函数节流的实现;
function throttle(fn, delay) {
  var preTime = Date.now();

  return function() {
    var context = this,
      args = arguments,
      nowTime = Date.now();

    // 如果两次时间间隔超过了指定时间,则执行函数。
    if (nowTime - preTime >= delay) {
      preTime = Date.now();
      return fn.apply(context, args);
    }
  };
}

规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll 函数的事件监听上,通过事件节流来降低事件调用的频率。

  • 柯里化

柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。

var add = function(x) {
  return function(y) {
    return x + y;
  };
};
var increment = add(1);
var addTen = add(10);
increment(2);
// 3
addTen(2);
// 12
add(1)(2);
// 3

这里定义了一个 add 函数,它接受一个参数并返回一个新的函数。调用 add 之后,返回的函数就通过闭包的方式记住了 add 的第一个参数。所以说 bind 本身也是闭包的一种使用场景。

柯里化是将 f(a,b,c) 可以被以 f(a)(b)(c) 的形式被调用的转化。JavaScript 实现版本通常保留函数被正常调用和在参数数量不够的情况下返回偏函数这两个特性。

  • 模块化

模块化的目的在于将一个程序按照其功能做拆分,分成相互独立的模块,以便于每个模块只包含与其功能相关的内容,模块之间通过接口调用

模块化开发和闭包息息相关,通过模块模式需要具备两个必要条件可以看出:

  • 外部必须是一个函数,且函数必须至少被调用一次(每次调用产生的闭包作为新的模块实例)
  • 外部函数内部至少有一个内部函数, 内部函数用于修改和访问各种内部私有成员
function myModule (){
    const moduleName = '我的自定义模块'
    var name = 'sisterAn'

    // 在模块内定义方法(API)
    function getName(){
        console.log(name)
    }
    function modifyName(newName){
        name = newName
    }

    // 模块暴露:  向外暴露API
    return {
        getName,
        modifyName
    }
}

// 测试
const md = myModule()
md.getName()    // 'sisterAn'
md.modifyName('PZ')
md.getName()    // 'PZ'

// 模块实例之间互不影响
const md2 = myModule()
md2.sayHello = function () {
    console.log('hello')
}
console.log(md) // {getName: ƒ, modifyName: ƒ}

解决闭包带来的问题

  1. 使用块级声明变量—let const
  2. 使用IIFE
  3. 使用foreach进行循环
  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值