《你不知道的JavaScript》阅读笔记(1)

167 阅读7分钟

《你不知道的JavaScript》阅读笔记(1)

一、作用域

1、Js在执行前都会进行编译,只不过通常编译后它马上就会执行,换句话说js的编译发生在代码执行之前的几微秒甚至更短的时间内

2、作用域:负责收集和维护所有声明的标识符组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限

3、作用域嵌套:如果对某个块内或者是某个函数内有声明了一个块或者函数就会形成作用域嵌套,如果从最里面的作用域LHS查询或者RHS查询都找不到变量则会往外层进行查询,直到全局作用域

4、LHS查询:变量出现在复制操作的左侧,并不是说是在‘=’的左侧,可以理解为找到这个变量并对其赋值 var a=2;对a进行LHS查询

5、RHS查询:变量出现在复制操作的右侧,可以理解为得到这个变量的值 console.log(a),就是对a进行RHS查询他需要知道a的值,

6、LHS查询如果在作用域内访问不到则会在全局作用域中创建一个变量,前提是非严格模式下,而RHS则会抛出ReferenceError异常,如果RHS查询找到了这个变量但是你对这个变量进行了一些非法操作,比如访问了空对象里的属性值,那么就会抛出一个TypeError异常

二、词法作用域

含义:字面意思理解,在词法阶段的作用域,也就是你写完代码后词法作用域就确定了,具体作用域取决于你怎么写,大多数情况下编译后作用域也是不变的

无论函数在哪被调用,他的词法作用域都在声明时就确定了

几种欺骗词法的情况:eval,setTimeout、setInterval第一个参数为字符串,new Function接受字符串,with,这些都是很不推荐的写法

三、函数作用域和块作用域

函数作用域:声明了一个函数,那么这个函数内所有变量在整个函数内部都可以被访问到

函数声明以及函数表达式区别:看Function关键字出现的位置,如果是声明的第一个词就是function,则它就是函数声明否则就是函数表达式(立即执行)

IIFE:(function(){})()

块级作用域:在es6之前的js其实是没有明确的块级作用域,但有一个特例就是try catch中的catch

分支具有块级作用域,es6中的let则是明确规定了用let声明的变量具有块级作用域,const也能创建块级作用域变量

四、提升

所有变量包括函数的声明在被执行前都会被编译器首先处理,一个简单的声明赋值可以理解为两步

var a=2;
可以理解为
var a;
a=2;
//其中var a就是变量声明会被提前处理,而a=2这个赋值则会留在原地等待执行

可以看到对于变量的声明在编译阶段就会执行,而赋值则仍会留在原地等待代码执行,这个过程就被称为提升.

注意:不是所有变量声明都会被提升到全局作用域,每个作用域内部都会进行各自的提升

函数声明会被提升,但是函数表达式不会被提升,这两个如何区分可以看第三章

函数声明的提升会比变量的提升优先级高,实例如下

foo()
function foo(){
	console.log(1)
}
var foo=function(){
	console.log(2)
}

上面代码会执行输出1,如果写成提升那么就相当于下面的代码执行

function foo(){
	console.log(1)
}
var foo//重复的变量声明会被忽略
foo()
foo=function(){
	console.log(2)
}

五、闭包

定义:函数可以记住当前的词法作用域,即使不在当前的词法作用域内执行,也可以访问该词法作用域内的变量,如下

function foo(){
	var a=2;
	function bar(){
	console.log(a)
	}
return bar
}
const baz=foo();
baz()

上述代码是一段很经典的闭包,如果套用我们定义怎么理解呢,首先bar在foo这个函数作用域内声明的,所以它可以记住foo这个作用域,所以可以访问里面的a,baz执行了foo其实就是拿到了bar的引用,然后我们在全局作用域内执行了baz,就可以理解为在全局作用域内执行了bar,我们依旧能够拿到foo作用域内的a,这就是典型的闭包

闭包还有个作用就是它会阻止垃圾回收器来释放不再使用的内存,一般来说一个函数执行完后垃圾回收器就会考虑将该函数的整个作用域都销毁,但由于闭包在这一步并不会消失,如上面的代码所示,一般foo()执行完后gc会考虑回收foo的作用域,但是由于baz(),gc并没有发生这一步,所以foo作用域内的a变量仍旧能被访问

无论你用了什么方法,让函数在定义的词法作用域之外的作用域内执行就会形成闭包

你有时候写的代码其实就是闭包,只不过你并不在意,如下

function wait(a){
	setTimeout(function timer(){
	console.log(a);
},1000)
}
wait(2)

上述代码其实就是闭包,timer可以访问wait函数作用域,在全局执行玩wait后,timer依然保留wait作用域的闭包,因此还是可以访问到变量a的

IIFE创建闭包,如下:

for (var a=1;a<3;a++){
	setTimeout(()={
		console.log(a)
}),1000
}
//使用闭包拿到每个迭代i的值
for (var a=1;a<3;a++){
	(function(j){
	setTimeout(()=>{
		console.log(j)
	},1000)
})(a)
}

模块:

function foo(){
	var a=1;
	function bar(){
	console.log(a)
}
	return {
	bar
}
}
const obj=foo();
obj.bar()

可以看到bar在外部执行能够访问到foo作用域内的值,实际上这就是我们举的第一个例子

模块在JS中有两个必要条件:1、具有外部的封闭函数,且至少被调用一次。2、封闭函数内返回一个函数,且该函数修改或者返回了内部的变量

上面代码foo其实可以被称之为模块创建器,每次执行完后都会产生新的实例对象,我们也可以使用IIFE来实现一个单例模式

const obj=(function foo(){
	var a=1;
	function bar(){
	console.log(a)
}
	return {
	bar
}
})()
obj.bar()

当然模块也接受传递参数

const obj=(function foo(a){
	function bar(){
	console.log(a)
}
	return {
	bar
}
})(2)
obj.bar()

如果你有学过面向对象的思想的话其实你就可以把一个函数当作class

简单模块管理器的实现

const m=(function module(){
		var module={}
		function define(name,deps,impl){
			for(let i=0;i<deps.length;i++){
				deps[i]=module[deps[i]]
			}
			module[name]=impl.apply(impl,deps)
		}
	function get(name){
		return module[name]
	}
	return {
		define,
		get
	}
})()
m.define('foo',[],function(){
	function dosomething(){
	console.log('foo')
	}
	return {
		dosomething
	}
})
m.define('baz',['foo'],function(foo){
	function doanything(){
		foo.dosomething()
}
	return {
		doanything
	}
})
const foo=m.get('foo')
const baz=m.get('baz')
foo.dosomething() //foo
baz.doanything() //foo

需要读者好好理解,比较绕,其中创建baz的时候首先baz依赖我们的foo,所以需要找到已经声明的foo模块,然后我们通过一句module[name]=impl.apply(impl,deps),使foo作为参数让baz来使用

现在的模块化机制:es module

其实上述所说的不管是模块还是IIFE,或者是其他函数形式都形成了闭包,闭包在你平常开发的时候肯定是无处不在的

六、动态作用域

首先要声明javascript并不具备动态作用域,动态作用域不关心你在哪声明的,他只关心你在哪执行的,他的作用域链是基于调用栈的,如下代码

function foo(){
	console.log(a)
}
var a=2;
function bar(){
	var a=3;
	foo()
}
bar()
//3
//2

如果javascript有动态作用域,上述代码会输出3,因为执行到foo()时不会从foo的词法作用域内开始查找变量a,由于foo是在bar内执行的所以会在bar的作用域内查找变量a,但其实他最终会输出2,他会在从词法作用域往上寻找,而动态作用域这点和我们的接下来讲的this会有很多相似的地方