作用域和闭包
该章节共包括以下几个知识点:
- 执行上下文
- this
- 作用域
- 作用域链
- 闭包
题目十:对于变量提升的理解
变量提升并不是很难的概念,从原理层面解释,变量提升是因为js在执行前会有一次“预编译”过程,由于这个过程的存在,会导致只要是变量声明形式的内容,都会被提前定义,但是不会被赋值。有点类似于这样:
// 编写代码
// source code
var a = 100
// 代码运行分为两个阶段
// pre-combiling
var a
// running
a = 100
于是就会导致一些看起来比较难以理解,其实很简单的现象:
console.log(a)
//console.log(b)
var a = 100
console.log(a)
// 执行顺序
var a
console.log(a)
//console.log(b) //如果加上这句,会报错(Error: b is not defined)
a = 100
console.log(a)
// 执行结果
undefined
100
从上面的例子可以看出来,undefined 和 is not defined 的含义不同。
另外有一个比较重要的知识点,就是对于函数而言,有两种定义方法,一种是函数声明,一种是函数定义:
function a() { // 函数声明
console.log("this is function a")
}
var b = function() { // 函数表达式
console.log("this is function b")
}
- 函数声明形式,会在预编译阶段直接被定义好,即不管这段代码出现在什么位置,在执行的时候都会被提升到预编译阶段。因此,不管这段代码出现在什么位置,都可以在程序的任何位置使用
- 而对于函数表达式的形式,类似于普通变量的定义,在这段代码出现之前,如果访问了b,会发现其值为undefined,因此只有在这段代码之后才能把它当做函数来使用。
题目十一:说明this的几种不同的使用场景
this的出现原因:
function printName() {
console.log(this.name)
}
var a = {
name: "wufan"
}
var b = {
name: "sundan"
}
printName.call(a) //wufan
printName.call(b) //sundan
从上面的例子可以看出来,如果不使用this的话,需要给函数传递一个上下文环境,例如下面的形式:
function printName(context) {
console.log(context.name)
}
printName(a)
printName(b)
虽然上面的形式看起来也很简洁,但是不得不说明的是,通过this来隐式的传递参数更为优雅。这并不是强词夺理,而是随着编程过程中代码量逐渐增加,使用的模式越来越复杂的时候,显式的传递会导致代码越来越混乱,使用this的话则不会
——整理自《你不知道的JavaScript(第二部分)》
但是我不太理解,因为如果不使用this的话不就是普通的函数编程吗?其他语言的编写不就是这样完成的吗?
题目十二:创建10个a标签,点击时弹出对应的序号
方法一:事件委托
<body>
<div id="container">
<a id="a1">a1</a>
<a id="a2">a2</a>
<a id="a3">a3</a>
<a id="a4">a4</a>
<a id="a5">a5</a>
<a id="a6">a6</a>
<a id="a7">a7</a>
<a id="a8">a8</a>
<a id="a9">a9</a>
<a id="a10">a10</a>
</div>
<script>
document.getElementById("container").addEventListener('click', function(e) {
e.preventDefault()
if (e.target) {
console.log(e.target.innerText)
}
})
</script>
</body>
或者循环事件绑定的形式:
for (var i = 0; i < 10; i++) {
(function(i) {
var a = document.createElement('a')
a.innerHTML = i + '<br>'
a.addEventListener('click', function(e) {
e.preventDefault()
console.log(i)
})
document.body.appendChild(a)
})(i)
}
下面给出一种错误写法:
for (var i = 0; i < 10; i++) {
var a = document.createElement('a')
a.innerHTML = i + '<br>'
a.addEventListener('click', function(e) {
e.preventDefault()
console.log(i)
})
document.body.appendChild(a)
}
结果为不论点击哪一个,输出都为10,因为click事件是一个非阻塞事件,会在点击的时候才发生。而点击的时候,i已经循环完毕,I的值为10,因此不论点击哪一个,结果都是10
修改方法:
将var i改为let i,并且要放在括号内定义i
for (let i = 0; i < 10; i++) {
var a = document.createElement('a')
a.innerHTML = i + '<br>'
a.addEventListener('click', function(e) {
e.preventDefault()
console.log(i)
})
document.body.appendChild(a)
}
上面的方法可行的原因是,let会创建块级作用域(ES6新特性),也就是同事创建了10个作用域分别用于执行click事件。
而上面使用立即执行函数进行封装的原理也是类似的,通过立即执行函数达到了将内部封装为一个函数作用域。
题目十三:如何理解作用域
JS没有块级作用域,只有函数作用域和全局作用域
ES6的新特性支持使用let,const等来创建类似于块级作用域的代码机制。
作用域是一套规则,用于确定在何处以及如何查找变量。如果查找的目的是对变量进行赋值,那么就会使用LHS查询,如果目的是获取变量的值,就会使用RHS查询。
——摘录于《你不知道的JavaScript(第一部分)》
题目十四:实际开发中闭包的应用
闭包的使用场景:将函数作为返回值,将函数作为参数进行传递。在这两种情况时,也就形成了闭包
闭包的应用主要是:可以用于封装变量,可以用于收敛权限(也就是函数的外面无法修改到内部的值,只能通过借口函数进行操作。)
// this is a simple example for closure(闭包)
function a() {
var x = 100
function b() {
console.log(x)
}
return b
}
var c = a()
c()
// this is another example
function wait(message) {
setTimeout(function timer() {
console.log(message)
}, 1000)
}
wait("Hello, closure!")
上面这个例子是将函数timer()当做参数进行的传递,从而产生了闭包。即timer()依然保有wait()作用域的闭包,即使在执行完之后也是。
只要使用了回调函数,其实就是在使用闭包
最后再写一个闭包应用的小功能,用于判断变量是否为第一次加载:
function isFirstLoaded(){
var _list = []
return function(id) {
if(_list.indexOf(id) >= 0){
return false
} else {
_list.push(id)
return true
}
}
}
var firstload = isFirstLoaded()
firstload(10) // true
firstload(10) // false
firstload(20) // true
firstload(30) // true
firstload(20) // false