本网站(662p.com)打包出售,且带程序代码数据,662p.com域名,程序内核采用TP框架开发,需要联系扣扣:2360248666 /wx:lianweikj
精品域名一口价出售:1y1m.com(350元) ,6b7b.com(400元) , 5k5j.com(380元) , yayj.com(1800元), jiongzhun.com(1000元) , niuzen.com(2800元) , zennei.com(5000元)
需要联系扣扣:2360248666 /wx:lianweikj
一文带你打通JavaScript的作用域链、闭包、this指向
itnanba · 277浏览 · 发布于2023-03-23 +关注

在我每次试图搞清楚JavaScript的作用域链、闭包、this这些概念时,总会陷入一种孤立当中,无法理清这三者之间的联系,今天我试图用变量查找这一条线来串起这三者之间的联系,理解其中的缘由。

通过两道题弄清作用域链

function add(a) {
  console.log(a + b)
}

var b = 1

add(2) // 3

这一道题答案是显而易见的,在 add 函数里访问变量 b 时,首先从函数作用域中查找是否存在变量 b,显然是不存在的,那怎么办?

那就探出头去,去上层作用域(全局作用域)查找 ,发现存在变量 b,打印结果为3。

在这个查找过程中,层层递进的作用域,就形成了一条作用域链。

那作用域链就这么简单吗?看下第二道题:

var name = 'xiaoming';

function showName() {
    console.log(name);
}

function changeName() {
    var name = 'xiaofang';
    showName();
}

changeName();


思考10秒,changeName()会打印什么?是xiaofang吗? 还是xiaoming?

答案是xiaoming。

按照一般思维去理解,showName执行时,首先会从自身函数作用域中查找name变量,发现不存在,那么就探出头去查找,发现showName是在changeName函数中执行的,所以到changeName函数作用域中查找,发现它定义了变量name,那么就打印xiaofang。但是这与实际运行结果不符,这是为什么呢?

这是因为作用域链的查找模式分为:词法作用域和动态作用域。

什么是词法作用域呢?

官方的解释:根据函数书写的位置来决定查找的上一层作用域。

这么说可能不太好理解,还是以上面的那个例子为例。

当showName没有变量name时,需要探出头去查找,那么上一层作用域是changeName函数作用域呢?还是定义showName函数所在的全局作用域?

实际上,JavaScript默认采用的是词法作用域,也叫静态作用域。

现在showName书写在全局作用域中,所以查找的时候按照词法作用域来查找,即探出头去到全局作用域,发现全局作用域中有name变量,所以打印的结果是xiaoming。

那什么是动态作用域呢?还是以上面的代码为例:

  • 在 showName 函数的函数作用域内查找是否有局部变量 name;

  • 发现没找到,于是沿着函数调用栈,在调用了 showName 的地方继续找 name。沿着函数调用栈查找,它的上一层作用域就是changeName 函数作用域,刚好changeName 里有一个 name,于是就打印xiaofang;

【总结一下】

词法作用域和动态作用域的区别其实在于划分作用域的时机

  • 词法作用域:在代码书写的时候完成划分,作用域链沿着它定义的位置往外延伸;

  • 动态作用域:在代码运行时完成划分,作用域链沿着它的调用栈往外延伸;

【变量查找规则】:JavaScript变量查找是沿着作用域链查找变量,作用域链的确定又是按照词法作用域的规则来的。

由作用域链引出闭包

通过一段代码来认识闭包:

function addABC(){
  var a = 1,b = 2;
  
  function add(){
    return a+b+c;
  }
  return add;
}

var c = 3

var globalAdd = addABC()

console.log(globalAdd()) // 6


在这个例子里,作用域嵌套的情况展示如下:

image.png

当执行globalAdd时就是在执行add方法,它访问了a, b, c三个变量,但是add函数没有定义这三个变量,所以需要探出头去查找,根据第一节的内容,我们知道它是遵循词法作用域的规则,即根据书写位置来确定查找的上一层作用域。

add书写的位置在addABC函数内,所以上一级作用域是addABC函数作用域,刚好这个函数定义了a, b变量,但是没有定义c变量,那么继续往上找,即到全局作用域中查找,发现了c变量,这样打印的结果就是6。

但是,这个例子有个特别之处是当执行globalAdd时,addABC函数已经执行完成了,变量a, b按道理来说已经被自动销毁了,但是我们现在仍然可以访问到a, b这两个变量,说明这两个变量没有被销毁,仍然存在内存当中。

这就是JavaScript中鼎鼎大名的闭包。那什么是闭包呢?

搞清楚什么是闭包,就需要搞清楚什么是自由变量。

像 a, b 这样在函数中被使用,但它既不是函数参数、也不是函数的局部变量,而是一个不属于当前作用域的变量,此时它相对于当前作用域来说,这就是自由变量。

像 add 这样引用了自由变量的函数,这些自由变量就构成了一个闭包。

所以,闭包就是一个来存放自由变量的地方,它的查找规则也是遵循作用域链的规则,无非就是闭包始终存在内存当中。

那闭包有什么用呢?下面通过两个例子来介绍闭包的应用。

模拟私有变量

下面通过一段代码来了解什么是私有变量:

class User {
    constructor(username, password) {
        // 用户名
        this.username = username
        // 密码
        this.password = password
     }
}

let user = new User('xiuyan', '123') 
user.password // 123


像登录密码这么关键且敏感的信息,竟然通过一个简单的属性就可以拿到。这就意味着,只要能拿到 user 这个对象,就可以非常轻松地知道密码,甚至改写的密码。

像 password 这样的变量,希望它仅在对象内部生效,无法从外部触及,这样的变量,就是私有变量,怎么实现呢?

在 JavaScript 中,既然无法通过 private 这样的关键字直接在类里声明变量的私有性,就只能另寻它法。

大家想想,在内部可以拿到、外部拿不到,这难道不就是函数作用域的特性吗?

所以,思路就是把私有变量用函数作用域保护起来,形成一个闭包。

/ 利用闭包生成IIFE,返回 User 类
const User = (function() {
    // 定义私有变量_password
    let _password

    class User {
        constructor (username, password) {
            // 初始化私有变量_password
            _password = password
            this.username = username
        }
        login() {
           // 为了验证 login 里仍可以顺利拿到密码
           console.log(this.username, _password)
           ...
        }
    }

    return User
})()

let user = new User('xiuyan', '123')
 
console.log(user.username) // xiuyan
console.log(user.password) // undefined 外界拿不到
console.log(user._password) // undefiend 外界拿不到
user.login() // xiuyan 123 通过login函数可以拿到


现在,user对外暴露的属性已经没有 password变量了。现在只能通过user内的login方法才能访问到password,也就是只能通过内部的方法才能访问,外部访问不到。

这样,通过闭包成功达到了用自由变量来模拟私有变量的效果。

到这里,我建议你停下来阅读后面的内容,仔细思考下上面闭包的使用,想一想在日常开发中你使用闭包的目的是什么?

其实,使用闭包的大部分场景都是为了构造一个私有变量,这个私有变量外部访问不到,只能内部才能访问,而且这个私有变量是始终存在内存中。很多开源库都使用了闭包这一特性。

函数柯里化

函数柯里化就是把接受 n 个参数的 1 个函数改造为只接受 1个参数的 n 个互相嵌套的函数的过程。即把fn(a, b, c) 变成 fn (a)(b)(c)。

它的主要功能是:可以让函数在必要的情况下帮我们 "记住" 一部分入参。

原函数:

function generateName(prefix, type, itemName) {
    return prefix + type + itemName
}


经过科里化后:

function generateName(prefix) {  
    return function(type) {
        return function (itemName) {
            return prefix + type + itemName
        }    
    }
}


这样一来,原有的generateName(prefix, type, name) 现在经过柯里化已经变成了 generateName (prefix)(type)(itemName)。

这样有什么好处呢?

好处是让你可以少传prefix, type这两个参数,让程序记住这两个参数,这样每次调用你只需要传入name参数即可。

this指向

首先思考下 JavaScript 为什么要有 this?

this的目的也是用来查找变量的,那为什么又作用域链查找变量,还需要this来查找变量呢?

要解答上面的问题,首先看一段代码:

var bar = { 
    myName:"xiaoming", 
    printName: function () { 
        console.log(myName) 
    } 
}
var myName = 'xiaofang'
bar.printName() // xiaofang


根据第一节所学的知识,可以很快得出答案是xiaofang。

因为 JavaScript 作用域链是由词法作用域决定的,而词法作用域是由代码结构来确定的。printName书写在全局作用域中,所以访问的myName也是在全局作用域中的。

不过按照常理来说,调用bar.printName方法时,该方法内部的变量 myName 应该使用 bar 对象中的,因为它们是一个整体,大多数面向对象语言都是这样设计。

所以,在对象内部的方法中使用对象内部的属性是一个非常普遍的需求。但是 JavaScript 的作用域机制并不支持这一点,基于这个需求,JavaScript 又搞出来另外一套 this 机制。

所以,在 JavaScript 中可以使用 this 实现在 printName 函数中访问到 bar 对象的 myName 属性了。

具体该怎么操作呢?你可以调整 printName 的代码,如下所示:

printName: function () { 
    console.log(this.myName) 
}


【总结】

现在我们知道了变量查找有两套机制:一套是遵循作用域链查找;一套是this机制查找。

this 指向谁?

一般情况下,this指向调用它的那个对象。

bar.print()
print()


第一行bar.print()里面是bar这个对象调用的,所以print函数里的this指向bar对象。

第二行print()可以看成是window.print(),所以print函数里的this指向window对象。

当调用方法没有明确对象时,this 就指向全局对象。在浏览器中,指向 window;在 Node 中,指向 Global

在三种特殊情境下,this 是一定会指向 window的:

  • 立即执行函数(IIFE)

  • setTimeout 中传入的函数

  • setInterval 中传入的函数

箭头函数中的this

箭头函数中的this比较特别,它认"死理" ———— 认"词法作用域"的家伙,即根据书写位置来约定this指向。

所以你会在有些文章中看到这样一句话:箭头函数中的this继承于它的上一层作用域的this,即父级的this。

因此,箭头函数中的 this,和你如何调用它无关,由你书写它的位置决定。例如:

var name = 'BigBear'
var me = {
  name: 'xiuyan',
  // 书写位置
  hello: () => {
      console.log(this.name)
  }
}

// 调用位置
me.hello() // BigBear


因为箭头函数在书写时,它所在的作用域是全局作用域,于是这个箭头函数里面的this就和全局对象绑在了一起。

箭头函数有一些普通函数没有的特点:

  • 没有arguments;

  • 无法通过apply call等改变this;

因为箭头函数的特点,在某些场景下我们不能使用箭头函数。

1. 对象的方法不能使用

this指向window,而不是obj

const obj = {
    name: 'xiaoming',
    getName: () => {
        return this.name
    }
}


2. 动态上下文中如果有this,不能使用

this指向不是btn,而是window

btn.addEventListener('click', () => {
    this.innerHTML = 'click'
})

3. vue中的生命周期和method不能使用

因为vue组件本质上就是一个对象,对象的方法是不能用箭头函数的,否则this就不是指向当前对象了。

react组件中是可以使用箭头函数的,因为它本质上是一个class,class里面是方法是可以使用箭头函数的。

如何改变this指向

改变 this 的指向,主要有两条路:

1. 通过改变书写代码的方式:箭头函数

var a = 1
var obj = {
  a: 2,
  // 书写位置
  showA: () => {
      console.log(this.a)
  }
}
// 调用位置
obj.showA() // 1

当我们将普通函数改写为箭头函数时,箭头函数的 this 会在书写阶段就绑定到它父作用域的 this 上。

无论后续我们如何调用它,都无法再为它指定目标对象,因为箭头函数的 this 指向是静态的,"一次便是一生"。

所以,虽然是obj.showA(),对象obj调用了showA,但是this仍然指向了window。

2. 显式地调用call, apply, bind 方法

改变 this 指向,常用的是 call、 apply 和 bind 方法:

  • call: fn.call(target, arg1, arg2) 改变后立即执行

  • apply: fn.apply(target, [arg1, arg2]) 改变后立即执行

  • bind: fn.bind(target, arg1, arg2) 改变后不立即执行

先来看一个call的调用范例:

var me = {
  name: 'xiuyan'
}
function showName() {
  console.log(this.name)
}
showName.call(me) // xiuyan

showName()调用时,this指向了window,打印结果为undefined;

showName.call(me)调用时,改变了this指向,此时this指向了me对象,所以,打印结果是xiuyan;

那call是如何改变this执行的呢?

下面来实现一个call函数,首先至少能想到以下两点:

  • call 是可以被所有的函数继承的,所以 call 方法应该被定义在 Function.prototype 上;

  • call 方法做了两件事:

    1. 改变 this 的指向,将 this 绑到第一个入参指定的的对象上去;

    2. 根据输入的参数,执行函数;

所以,代码实现如下:

Function.prototype.myCall = function(context, ...args) {
    // step1: 把函数挂到目标对象context上
    context.func = this
    // step2: 执行函数
    context.func(...args)
    // step3: 删除 step1 中挂到目标对象上的函数,把目标对象”完璧归赵”
    delete context.func
}
  1. 函数赋值:context.func = this

这一段代码稍微不好理解,当我们调用fn.myCall时,此时myCall函数里面的this就指向fn,在JavaScript中函数也是对象。

所以,我们在context上定义了一个属性func,它的值为函数fn;

  1. 调用函数:context.func(...args)

此时func函数是被context调用的,所以func里面的this执行就指向了context,也就是我们传入的第一个参数,这样就改变了this指向。

  1. 删除函数context.func

总结

本文通过变量查找这一条线开始讲述了JavaScript是如何访问变量的。

首先,变量是根据作用域链查找,而作用域链的确定根据词法作用域的规则,即代码的书写位置来确定。

在根据作用域链查找过程中,又引出了一类特殊的变量,即自由变量。它们虽然定义在函数内,但是随着函数执行完,它们并不会被垃圾回收,这是因为有其他的函数引用了这些变量,这就是闭包。

接着,使用私有变量的实现和函数柯里化两个例子阐述了闭包在开发中使用场景。说到底闭包就是一个私有变量,外部不能访问,只能内部访问。

但是,当对象调用自己的方法时,不能访问自己的属性,反而是访问了全局的属性,这就比较反人类了,所以就引出了this访问机制,它和作用域链构成了JavaScript访问变量的全部内容。

接着讨论了箭头函数中的this,它是根据书写位置确定,一旦指定就不会改变this指向,根据它的这个特性,所以需要在一些场景下不能使用箭头函数。

最后,通过一个模拟call函数的实现,弄清楚了call, apply, bind改变this指向的原理。


相关推荐

PHP实现部分字符隐藏

沙雕mars · 1325浏览 · 2019-04-28 09:47:56
Java中ArrayList和LinkedList区别

kenrry1992 · 908浏览 · 2019-05-08 21:14:54
Tomcat 下载及安装配置

manongba · 970浏览 · 2019-05-13 21:03:56
JAVA变量介绍

manongba · 962浏览 · 2019-05-13 21:05:52
什么是SpringBoot

iamitnan · 1086浏览 · 2019-05-14 22:20:36
加载中

0评论

评论
分类专栏
小鸟云服务器
扫码进入手机网页