前言
- 词法作用域
- 作用域链
- this的使用
刚开始我学这三个概念的每一个概念是分开来看,我以为学懂了。
但是后面做面试题的时候,我发现我总是做错,我就不断回顾这些知识,我发现他们名字都有些相似,特性也相似,而且在学习的时候没有把它们串联起来导致我思路比较混乱。
所以写下这篇文章帮组我更好的理清思路,也帮助再看的各位少走一点弯路。
正文
-
词法作用域
-
词法作用域
词法作用域看起来是一个高大上的说法,其实也就那么一回事。
也就是我们经常谈到的作用域,以及根据作用域延伸出来的作用域链。
作用域一般分为两种
- 词法作用域:也称为静态作用域。这是最普遍的一种作用域模型,也是我们学习的重点
- 动态作用域:相对“冷门”,但确实有一些语言采纳的是动态作用域,如:Bash 脚本、Perl 等
我们先来看一段代码
let name = "Barry Song" function hi() { console.log(name); } function hiSuperman() { let name = "Clark Kent" console.log(name); } function hiBatman() { let name = "Bruce Wayen"; hi(); } hi();// Barry Song hiSuperman(); //Clark Kent hiBatman();//Barry Song相比有不少小伙伴下意识会认为hiBatman(),会打印出Bruce Wayen,实际上hi()根据词法作用域的特性,它在创建的初始,它的作用域是根据它定义的位置。
词法作用域判定在于它定义的位置
所以我们如果在hiBatman()里面定义hi()的话,hi的作用域就是hiBatman(),自然能取到Bruce Wayen。
function hiBatman() { let name = "Bruce Wayen"; function hi() { console.log(name); } hi(); } -
作用域链
我们接着上面的代码来看,稍作修改
let name = "Barry Song" function hiBatman() { // let name = "Bruce Wayen"; function hi() { console.log(name); } hi();// Barry Song }我把hiBatman()里面的name注释掉了。
这次它再次打印出了Barry Song。
我们再来结合词法作用域的特性来看作用域链。
当我们找不到一个变量的时候,往往会向它本身定义的位置向上找。
hi()定义的位置是在hiBatman()里面,hiBatman()里面并没有name这个变量,那么我去找定义hiBatman()的定义域,是全局定义域,里面刚好有一个name。
-
一个小补充关于函数调用栈
var foo = 'foo' function testA() { console.log('执行第一个测试函数的逻辑'); testB(); console.log('再次执行第一个测试函数的逻辑'); } function testB() { console.log(foo); } testA();我们都知道函数的调用时将函数压入栈中,它管理的是执行顺序。
我为什么要提这个,因为我刚开始在学这两个概念的时候,老是把作用域链和函数调用栈搞混搞混,认为栈顶的函数能调用上一个函数的作用域里面的变量。作用域链是在书写的时候就定义好了。
-
-
this的使用
回想一下什么时候会用this?
在构造函数里面初始化对象属性的时候,或者在对象字面量的方法里面调用对象的属性的时候。
所以this要根据对象(滑稽)来看。
前置小知识,node环境下的顶层对象是global,浏览器的环境的顶层对象是window!
在浏览器的环境下
-
ES5 中顶层对象的属性等价于全局变量。
-
ES6 中有所改变:var、function 声明的全局变量,依然是顶层对象的属性;let、const、class 声明的全局变量不属于顶层对象的属性,也就是说 ES6 开始,全局变量和顶层对象的属性开始分离、脱钩。

根据上面的情景分为两类
-
对象字面量
var a = a let Test = { a: 1, func1: () => { console.log("箭头 => a: " + this.a); }, func2() { console.log("直接命名 => a: " + this.a); }, func3: function () { console.log("键值对=> a: " + this.a); }, func4() { return function () { console.log("闭包函数 => a: " + this.a); }; }, };在对象字面量里面定义函数的方式有很3种,普通键值对,箭头函数键值对,直接名字定义,还有一种最特殊的闭包。
我们分为两种方式来调用函数,一种是直接调用,一种是把函数值传给其他变量,然后调用被赋予的那个变量。
console.log("\n 直接调用函数 ----------- \n"); Test.func1();//箭头 => a: 3 Test.func2();//直接命名=> a: 1 Test.func3();//键值对 => a: 1 Test.func4()();//闭包函数 => a: 3这是因为箭头函数没有this,就算用bind和apply,call也不行,具体看MDN,所以箭头函数的this会绑定父级上下文。
var a = 1; let fn = () => { console.log(this.a); } fn();// 1所以
Test.func1();//箭头 => a: 3 Test.func4()();//闭包函数 => a: 3这里执行的箭头函数会找到外层的this也就是全局对象,获得a,闭包函数也比较特殊和箭头函数一样,会绑定父级的this,所以在使用闭包的和箭头函数的时候要特别注意this的使用。
我们再来看赋值调用
console.log("\n 赋值调用函数 ----------- \n"); let func1 = Test.func1; let func2 = Test.func2; let func3 = Test.func3; let func4 = Test.func4(); func1();//a: 3 func2();//a: 3 func3();//a: 3 func4();//a: 3这里就比较有趣了,全部都是3。
我们都知道在对象是一种键值对的散列数据结构,里面的所有都是以**[[key]] : value** 的方式存储的,当我们把value复制给另外一个变量的时候,我们执行这个新的变量是在全局之下执行的,它的前面并没有调用它的对象,所以他的this就变成全局对象。
-
构造函数
在创建new这个关键字创建一个对象发生了那些事。
- 现在内存中创建一个对象
- 这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性
- 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
- 执行构造函数内部的代码(给新对象添加属性)
- 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象
var a = 3; function Fn() { this.a = 2; this.hi = function () { console.log(this.a); } } let fn = new Fn(); let hi = fn.hi; fn.hi();// 2 hi();// 3关于构造函数的this使用指南和上面的对象字面量如出一辙,注意new创建对象时的步骤就好了。
-
-
改变this
js能使用三个方法call,apply,bind三个方法来改变this的指向

如何手写着三个方法
- call / bind 两者比较类似,只不过是参数的不同
Function.prototype.myCall = function (context, ...args) { context.func = this; context.func(...args); delete context.func; }-
bind
bind比较麻烦一点要使用到闭包
Function.prototype.myBind = function (context, ...args) { let that = this; return ( function () { context.func = that; context.func(args); delete context.func; } ) }这里就用到了,上面所说的this的指向问题,闭包是比较特殊的函数,它取到的this是在返回之后外面那一层的this,所以这里要提前把this存到that里面去。
-
总结
-
词法作用域是对变量的访问,跟定义的位置有关系,作用域链则是定义位置的镶套,当当前无法访问到变量的时候,则会根据作用域链往上找。
-
函数调用栈是函数执行的顺序,容易和作用域链搞混。
-
this的使用
普通的调用函数,this会跟着调用的对象走。闭包和箭头函数则会绑定父级上下文,并且箭头函数是不能被改变this的。
call,apply ,bind可以改变this。
当函数使用构造函数时,this指向new 创建的那个新对象。
-