学习js也有一段时间了,可是往往提到执行上下文、做用域、闭包、变量提高、this等关键词时心中老是有一个模糊的概念,好像知道又好像不知道,所以我想和你们系统的讨论这几个概念。但愿可以帮到和我同样还为这几个熟悉而陌生的词感到苦恼的同窗!javascript
这里我不打打算一开始就讨论上面那些概念,每种语言都有内建的数据类型,不一样的创建方式也意味着不同的使用方式。而是从js的数据类型开始一步一步分析,则可让你摸清楚上面几个概念的前因后果。前端
若是你细心的留意过js语言的一门细节,就会发现js是一门弱类型的动态语言。vue
在js代码会在if判断语句中自动将表达式计算成布尔类型的值,同时在js中声明的变量的定义无需肯定它是字符串、数字或者布尔等其余类型,这意味着你能够在一个变量中保存不一样类型的数据。值得一提的是这种动态语言的类型再带来极大便利性的同时也会带来一些使人困扰的问题,在vue这门优秀的框架中使用了Flow对js作了静态类型语言检查。java
上面咱们知道了js是一门弱类型的动态语言,那么咱们接下来看看js中的数据类型python
在js中数据类型分为基本类型和引用类型:面试
(1)基本类型有:浏览器
(2)js的引用类型是从object的子类型,有以下几种:缓存
js中对不一样类型的数据的操做不是相同的,要想理解其中的差别,先得搞清楚js中存储模型。(从极客上拿的图)markdown
在js执行的过程当中,主要有三种类型内存空间,分别是:代码空间、栈空间、堆空间闭包
基本类型的数据类型都存储在栈空间,引用类型的值保存在堆中的。
// 定义四个变量
var num1 = 5
var num2 = num1;
var obj1 = {
name: '小猪皮皮呆'
}
var obj2 = obj1
// 修改num1和obj1
num1 = 4
obj1.name = '小猪'
// 输出四个变量
console.log(num1) // 4
console.log(num2) // 5
console.log(obj1.name) // 小猪
console.log(obj2.name) // 小猪
复制代码
上面代码num1和num2的输出咱们可以很好的理解,由于在js中基本类型的数据类型都存储在栈空间。若是一个变量向另外一个变量赋值基本类型的值,会在变量对象上建立一个新值,而后把该值复制到为新变量分配的位置上。
那么为何obj1和obj2的name输出的结果都改变了呢?这是由于在js中引用类型的值保存在堆中的。若是一个变量向另外一个变量赋值引用类型的值,一样会在变量对象上建立一个新值,而后把该值复制到为新变量分配的位置上,但与基础类型不一样的是,这个值是一个指针,这个指针指向了堆中的同一个对象,所以在修改其中任何一个对象都是在对同一个对象修改。
看完上面的内容,相信你对栈和堆已经有了必定的理解,接下来咱们来看看js中传递参数的方式。在js中,全部函数的参数都是按值传递的,也就是说把函数外部的值复制给函数内部使用,就像把值从一个变量复制到另外一个变量里同样。
这就意味着,基本类型值得传递和引用类型值的传递就如同上述所说的复制过程是同样的。
// 基本类型的传递
function addTen(num){
num += 10
return num
}
var count = 20
var result = addTen(count)
alert(count) //20
alert(result) //30
// 引用类型的传递
function setName(obj) {
obj.name = "小猪皮皮呆"
}
var person = {}
setName(person)
console.log(person.name) // 小猪皮皮呆
复制代码
在这里有些同窗可能会将引用类型传递参数的方式搞错,会发出疑问:访问变量有按值和按引用两种方式,为何传递参数只有按值传递?
对于上例的基础类型的值的传递能够很容易的理解,可是引用类型的传递在局部中的修改会在全局中反应出来,会有同窗误觉得引用类型的传递是按参数传递的。但其实真正的过程是这样的:
从上述的过程当中,能够看出来,person这个变量是按值传递的。咱们再看个例子来讲明这个问题
function setName(obj){
obj.name = "小猪皮皮呆"
obj = new Object()
obj.name = "三元大神"
}
var person = {}
setName(person)
alert(person.name) // 小猪皮皮呆
复制代码
若是是按引用传递,显示的值应该是“三元大神”,但js中的引用类型的传递也是按值传递的,因此打印出来的是“小猪皮皮呆”。
看到这里,确定不少人要开始骂了,这我的标题党啊,开头说了要理清楚执行上下文、做用域、闭包、变量提高、this这些东西,怎么到如今还只字未提。都已经看到这里了,别着急!本文的思路是自顶向下的,从最外层你熟悉的地方开始讲起,慢慢的渗透到底部的各个概念,将各个知识点串在一块儿,造成知识体系。
showName() // 小猪
console.log(myName) // undefiend
var myName = "小猪皮皮呆"
function showName() {
console.log("小猪")
}
复制代码
上面代码的执行结果相信你们都不意外,这就是咱们耳熟能详的变量提高,可是他的内部到底发生了些什么,才会出现这种结果呢?
不少地方给出的解释是js代码在执行的过程当中,js引擎会把变量的声明部分和函数的声明提高到代码的开头部分。变量被提高后会设置默认值,也就是undefined。 这种说法没有错,可是咱们要更深刻的去看看这个所谓的变量提高内部发生了什么。
接下来咱们要开始咱们便进入了本文的重点部分,js代码的执行流程分为两部分:编译和执行。
看到这里,终于迎来了咱们要讨论的第一个重点:什么是执行上下文?
执行上下文的建立分为三种状况:
而在js中,上下文的管理则由调用栈负责,js执行过程当中三种内存空间之一的栈空间。咱们来看看它是如何负责的:
看到这里咱们即可以回答以前的问题了。所谓的变量提高就是js代码执行的过程当中,会先将代码进行编译,编译的过程当中变量的声明和函数的声明会被放入调用栈中造成上下文调用栈,剩余下的会生成执行代码。这就形成了变量提高的现象。
顺带一提,调用栈的大小有限,若是入栈执行的上下文超过必定数目,js引擎就会报错,这种现象就叫栈溢出,看下面一段代码:
function stackOverFlow (a, b) {
return stackOverFlow (a, b)
}
console.log(stackOverFlow(1, 2))
复制代码
看到这里,相信你已经理解了什么是执行上下文,什么是变量提高。是否是很简单呢?接下来我会带领同窗们继续看剩下的几个概念,有了上面的基础,剩下的内容则更好理解。
在上文中咱们已经了解了变量提高,因为 js 存在变量提高这种特性,从而致使了不少与直觉不符的代码,这也是 JavaScript 的一个重要设计缺陷。
var name = "小猪皮皮呆"
function showName(){
console.log(name);
if (0) {
var name = "小猪"
}
console.log(name)
}
showName()
// undefined
// undefiend
复制代码
在咱们熟悉调用栈后,在执行到showName时,会生成一个showName()的上下文,里面会将函数内部的name放入变量环境中并赋值undefined,因此第一个console没有打印出“小猪皮皮呆”,第二个打印以前由于if语句里面的语句没有执行,因此打印出的依然是undefined。
(1)做用域
而为何会存在这种特性还得从做用域提及,js中存在三种做用域,ES6以前只两种做用域:
(2)做用域链
这段代码很容易让人以为会打印结果会是“小猪皮皮呆”,这和咱们接下来要提到的另外一个概念做用域链有关
function bar() {
console.log(name)
}
function foo() {
var name = "小猪皮皮呆"
bar()
}
var name = "小猪"
foo() // 小猪
复制代码
相信前面的执行上下文部分同窗们已经理解了,接下来咱们会结合执行上下文来看做用域链:
(3)块级做用域
上面提到了ES5以前只有全局做用域和函数做用域,ES6为了解决变量提高带来的问题,引入了块级做用域。这个你们都很熟悉,可是js如何作到即支持变量提高的特性又支持块级做用域呢?
咱们继续从执行上下文的角度解决这个问题
function foo() {
var a = 1
let b = 2
{
let b = 3
var c = 4
let d = 5
console.log(a)
console.log(b)
}
console.log(b)
console.log(c)
console.log(d)
}
foo()
复制代码
上述造成的新的做用域链即是js对变量提高和块级做用域同时支持的实现。
一个常见的问题:如何解决下面的循环输出问题?
for(var i = 1; i <= 5; i ++){
setTimeout(function timer(){
console.log(i)
}, 0)
}
复制代码
for(let i = 1; i <= 5; i ++){
setTimeout(function timer(){
console.log(i)
}, 0)
}
复制代码
在了解了做用域链后再去理解闭包就十分简单了!
ES5中存在两个做用域:全局做用域、函数做用域,函数做用域会在函数运行结束后自动销毁 做用域链:查找一个变量时会从自身的做用域开始沿着做用域链一直向上查找 闭包:利用了做用域,能够将函数内部的做用域的变量访问到
(1)闭包如何产生:
const a = 2
function out () {
let a = 1
return function b () {
console.log(a)
}
}
const b = out()
b() // 1
复制代码
var a = 1
function bar(fn) {
var a = 2
console.log(fn)
}
function baz() {
console.log(a)
}
bar(baz) // 1
复制代码
// 定时器
setTimeout(function timeHandler(){
console.log('111');
}, 100)
// 事件监听
$('#app').click(function(){
console.log('DOM Listener');
})
复制代码
var a = 2;
(function IIFE(){
// 输出2
console.log(a);
})();
复制代码
IIFE(当即执行函数表达式)建立闭包, 保存了全局做用域window和当前函数的做用域,所以能够全局的变量。
for(var i = 1; i <= 5; i ++){
(function(j){
setTimeout(function timer(){
console.log(j)
}, 0)
})(i)
}
复制代码
(2)应用场景:
函数柯里化、前端经典面试题解密-add(1)(2)(3)(4) == 10究竟是个啥?
function add (...args) {
return args.reduce((a, b) => a + b)
}
function currying(fn) {
let args = []
return function _c (...newArgs) {
if (newArgs.length) {
args = [...args, ...newArgs]
return _c
} else {
return fn.apply(this, args)
}
}
}
let addCurry = currying(add)
let total = addCurry(1)(2)(3, 4)(5, 6 ,7)()
console.log(total) // 28
复制代码
(3)缺点:全局使用闭包会形成内存泄漏,因此尽可能少用
在上面一小节中咱们介绍了bar编译好了后outer就指向全局上下文,所以打印的不是foo()内部的“小猪皮皮呆”,大多数人会产生这样的异或即是将this和做用域链的概念弄混了。
而真实状况是,做用域链这套机制不支持咱们直接得到对象内部的变量,而又独立的成立了一套新的机制,绝对不要将二者混为一谈!
var obj = {
name: "小猪皮皮呆",
showName: function () {
console.log(name)
}
}
var name = "小猪"
obj.showName() // 小猪
复制代码
上面是一个经典的面试题,输出的结果是“小猪”而不是内部的“小猪皮皮呆”,有了以前对上下文和做用域链的理解,能够很容易的去解释,不在此赘述。
再强调一遍:做用域和this之间没有任何关系!this单独存在于执行上下文中,和执行上下文中的变量环境、词法环境、outer是并行的关系。
那么this要如何使用呢?若是想上述代码输出内部的name,即可以使用this来实现。
var obj = {
name: "小猪皮皮呆",
showName: function () {
console.log(this.name)
}
}
var name = "小猪"
obj.showName() // 小猪皮皮呆
复制代码
接下来再对this的指向作一个总结:
var person = {
name: "小猪皮皮呆",
changeName: function () {
setTimeout(function(){
this.name = "小猪"
}, 100)
}
}
person.changeName()
复制代码
上述代码想要经过changeName方法修改person内部的name属性,可是该代码存在一些问题,咱们便根据上述对this指向的总结来解决这题。
(1) 缓存内部的this
var person = {
name: "小猪皮皮呆",
changeName: function () {
var self = this
setTimeout(function(){
self.name = "小猪"
console.log(person.name)
}, 100)
}
}
person.changeName() // 小猪
复制代码
(2) 使用call、apply或bind显示绑定
var change = function () {
this.name = "小猪"
}
var person = {
name: "小猪皮皮呆",
changeName: function () {
setTimeout(function(){
change.call(person)
console.log(person.name)
}, 100)
}
}
person.changeName() // 小猪
复制代码
(3) 使用箭头函数
var person = {
name: "小猪皮皮呆",
changeName: function () {
setTimeout(() => {
this.name = "小猪"
console.log(person.name)
}, 100)
}
}
person.changeName() // 小猪
复制代码
好啦,到此为止文章开头提到的那几个词已经为你们梳理过一遍了,若是以为还不错的话,给小猪皮皮呆一个👍吧!
参考文献
- 《js高级程序设计》第三版
- 极客的专栏
- 《你不知道的js》
- 偶像神三元的博客