你不知道的javascript第一部分
第一章 --- 作用域
1.1编译原理
在这段部分,书描述了一个概念, 就是js尽管被归类为解释执行语言, 但实际上它是一门编译语言,但同时它的行为和常规编译语言又不相同, 书作者为什么这么说,因为js有在代码的执行阶段前,有一段非常短暂的编译操作(在这个阶段会进行变量提升,确定作用域),但它不需要在运行前去生成一个特定于操作系统的可执行文件,它是由解释器直接加载源代码并执行的,所以更准确的说,js是一门包含即时编译的解释型语言
1.2 左值右值
这段描述了编译器在处理关于 var a = 2 这个语句时的处理,编译器首先会询问作用域是否有一个名为a的变啦,如果有,则该声明会被忽略,否则它会要求作用域在当前作用域集合中声明一个新的变量, 命名为a,
这个部分发生在编译阶段, 在执行阶段的时候,引擎会询问a在作用域中是否有这个变量,如果没有就往上一级继续查找,
当引擎最后找到的时候,就会把2赋值给a这个变量,要是没有找到,会抛出一个异常,
在这个过程中,对于 var a = 2, 我们会把引擎对a的查询操作称之为左值查询(LHS), 同样的,对于 var b = a;, 我们会把a 称之为右值查询(RHS),,LHS和RHS区别不仅仅在于=的两边,更多的在于,RHS只是简单的查找某个变量,而RHS更多的是要找到这个变量容器本身并进行赋值操作。
javascript
function foo(a) { // 这里的a进行一次LHS查询,把2 赋值给a
console.log(a);// RHS查询
}
foo(2);//对foo进行一次RHS查询
LHS和RHS还有一个区别,在变量未定义的时候
scss
foo() {
a = 1
}
foo();
//对a 进行一个LHS查询, 但是这个变量在之前没有被定义
//在非严格模式下,会自动创建一个全局的a变量,对于严格模式
//会抛出一个 ReferenceError
console.log(a);
//如果是RHS,直接抛出ReferenceError
//在非严格模式下,LHS和RHS行为略有不同,
//对于RHs查询到的变量进行不合理的操作,会抛出typeError
function foo() {
}
foo()();
测验部分
javascript
function foo(a) {
var b = a;
return a + b;
}
var c = foo(2);
//找到所有的RHS查询 foo, b = a(其中的a), a + b(a和b)
//找到所有的LHS查询 c, a = 2,(其中的a) b = a (其中的b),
1.3作用域嵌套
在js中,只有函数, let , const, 会开启一个局部作用域,当作用域定义在另一个作用域内部就发生了嵌套,在嵌套的只作用域内部查询一个变量遵循由内到外的规则,当在某一个作用域找到了想要的变量,那么引擎就不会接着往上寻找了
部分总结
RHS不成功会抛出ReferenceError,不成功的LHS在非严格模式下会隐式创建全局变量,在严格模式下抛出ReferenceError
第二章 - 词法作用域
作用域分为词法作用域和动态作用域, 区别在于词法作用域在代码写完的时候就是确定的后面就不变了, 而动态作用域, 直到运行的时候才会被确定, 在本书中介绍了两种办法去修改静态的作用域,with, eval,基本没人用了
第三章 - 函数作用域和块作用域
函数作用域是由函数开启的, 块作用域由let, const开启
3.2 隐藏内部实现
基于最小暴露原则,应该尽量将变量或函数包括在作用域内,这样可以防止污染全部变量名.
javascript
function doSomething (a) {
b = a + doSometingEls( a * 2);
console.log(b * 3);
}
function doSomethingEls (a) {
return a - 1;
}
var b;
doSomething(2);//15
//像这里,一下子暴露了三个标识符出去,这是非常危险的一种行为
function doSomething ( a) {
var b = a + doSomethingEls(a * 2);
function doSomethindEls(b ) {
return b - 1;
}
console.log(b);
}
3.3函数作用域
js还提供了一种函数表达式的写法,在不需要函数名的情况下,连函数名都省去了,如果有函数名,也不会被放在全局作用域下
javascript
(function doSomething ( a) {
var b = a + doSomethingEls(a * 2);
function doSomethindEls(b ) {
return b - 1;
}
console.log(b);
})()
//doSometthing只在它的内部函数作用域内有定义,外部访问不到doSomething
这里也可以写成匿名函数的形式,但是有两点缺点,一件是它无法在内部调用,一件是不具备语义化,基于这两点,坚决不推荐使用匿名函数
3.3.2 立即执行函数表达式
下面展示IIFE的两种写法
javascript
(function foo() {
var a = 3;
console.log(a);
})()
(function IIFE(){
def(window);
})(function def (global) {
var a = 3;
console.log(a);
console.log(global.a);
})
3.4块作用域
块作用域细化了变量的封装, 像 var在for循环中由于函数异步执行导致的错误,通过块级作用域的封装得到了解决, js在es6前是没有块作用域的,在这之前,要使用块级作用域,只能通过
javascript
try {
throw '我是一个错误';
} catch(err ) {
var a = 1;
console.log(a, err); // 1 我是一个错误,
}
console.log(a);//1
//这和正常的块级作用域行为是相同的, var定义的变量突破了块级作用域,而err具有块级作用域,被局限在块级作用域内了
3.4.3 let
let 可以劫持其所在的作用域,为其变量创建一个块级作用域
ini
{
let a = 2;
console.log(a);//2
}
console.log(a);//ReferenceError
块级作用域有一个显式的好处,就是可以让js垃圾回收机制提早回收一个变量
javascript
function proccess(data) {
....
}
let someData = {}
proccess(someData);
oBtn.addEventListemer('click', function () {
.....
}, false);
这里someData在一个活跃的作用域内部,永远也不会被卸载
javascript
function proccess(data) {
....
}
{
let someData = {}
proccess(someData);
}
oBtn.addEventListemer('click', function () {
.....
}, false);
console.log(someData)//Reference
//执行完proccess作用域结束,someData就被卸载了
let最典型的比起var的优势还是在于for循环中
javascript
for(let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
}, 0);
}
for(var i = 0; i < 5; i++) {
(funciton (idx) {
setTimeout(function () {
console.log(i);
}, 0)
})(i)
}
上面这两段代码执行结果一致,for循环的let将i不断更新并且绑定到了循环的每一个迭代中
思考部分 - 如何使用es5模拟es6的块级作用域
javascript
{
let a = 10;
console.log(a);
}
//利用catch的类似于块作用域的功能
try {
throw 10
} catch(value) {
console.log(value);
}
//使用函数作用域模仿
(function () {
var a = 10;
console.log(a);
})()
第四章 - 提升
提升就是在js的编译阶段,我们用var定义的变量会被收集起来,在执行阶段的时候,即使在var之前使用了var定义的变量,也可以得到一个undefined,这个过程就像var a = 1;中把声明和赋值分开了,把声明放到作用域最顶端一样
javascript
a = 2;
var a;
console.log(a);//2
console.log(b);//undefined
var b;
函数声明也可以得到提升, 但是函数表达式不会被提升
javascript
foo();//正常调用
function foo() {
console.log(a);//undefined
var a = 2;
}
foo2();//TypeError 查到了,但是为undefined
bar();//ReferenceError, 没有这个标识符
var foo2 = function bar () {
}
bar();//ReferenceError
//在这里foo2会被提升,作为函数表达式的bar不会被提升
//所以可以看到,foo2没有抛出ReferenceError却抛出了一个
//TypeError,原因是对一个undefined使用了函数调用的语法
//而bar不会进行提升,所以抛出ReferenceError
//同时bar也无法在赋值之后使用,bar这个标识符只能在它自己函数体内部使用
4.3函数优先
所谓函数优先指的是作用域会先提升函数,然后提升变量,这其中变量如果有和函数名重复的,变量定义将被丢弃,但是重复的函数声明标识符会被覆盖掉
javascript
foo() // 1
var foo;
function foo() {
console.log(1);
}
foo = function () {
console.log(2);
}
对于定义在条件语句,循环语句内部的函数,处理和在全局作用域和函数作用域下不同,函数定义会被提升, 但是赋值不会被提升
javascript
foo();//TypeError
var a = true;
if(a) {
function foo() {
console.log('a');
}
} else {
function foo() {
console.log('b');
}
}
func();// TypeError
for(var i = 0; i < 5; i++) {
function func () {
console.log('func')
}
}
第五章 - 作用域闭包
先看一个闭包的例子
javascript
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz();//2
function foo() {
var a = 2;
function baz() {
console.log(a);
}
bar(baz)
}
function bar(fn) {
fn();
}
foo();
var fn;
function foo() {
var a = 2;
fucntion baz() {
console.log(a);
}
fn = baz;
}
function bar() {
fn();
}
foo();
bar();//2
什么是闭包,我觉得可以简单的认为,所谓的闭包就是一个函数返回了一个函数,这就可以称之为闭包了,因为这个返回的不仅仅是一个函数,还是对内部这个作用域的引用,返回一个函数,外部的函数就被内部的函数抱住了,无法被回收了 ,闭包有两个特征,1.为创建内部作用域内创建了一个包装函数,2.这个包装函数返回的值包含了对一个内部函数的引用,这样就会创建一整个包含内部作用域的闭包
模块
模块是一种利用了闭包的机制,模块必须满足两个条件,一个是必须有外部的封闭函数,这个函数至少被调用一次,封闭函数必须返回一个内部函数
javascript
function CoolModule() {
var something = 'cool';
var another = [1, 2, 3];
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(anthoer.join(','))
}
return {
doAnother,
doSomething
}
}
下面实现一个模块制作器
javascript
var createModules = (function Manager() {
var modules = {};
function define(name, deps, func) {
for(var i = 0; i < deps.length; i++) {
deps[i] = modules[deps[i]];
}
modules[name] = func.apply(func, deps[i]);
}
function get(name) {
return modules[name];
}
return {
get,
define
}
})();
createModules.define('bar', [], function () {
function hello (content) {
return 'hello' + content;
}
return {
hello
}
});
createModules.define('foo', ['bar'], function () {
var name = 'foo'
function hello2 () {
console.log(bar.hello(name), 'foo');
}
return {
hello2
}
})
var bar = createModules.get('bar');
var foo = createModules.get('foo');
console.log(bar.hello('bar'));
foo.hello2();//hellofoo foo;
像这种基于函数的模块,一个模块返回什么,它都是不确定的,模块的api只有在运行的时候才会被考虑,因此我们可以在运行的时候修改一个模块的api, 相比之下,es6的模块机制是静态的,在写完代码后api就确定了,编译器在编译阶段就能分析api并抛出错误,
附录A-动态作用域
js只有词法作用域,动态作用域和词法作用域的一个区别在于动态作用域链是基于调用栈的,词法作用域链基于作用域嵌套, 词法作用域是在写代码的时候确定的,动态作用域知道调用时确定,js尽管没有动态作用域机制,但是它的this的行为高度和动态作用域类似
附录C this词法
在这章提出了一个关于function胖函数的使用方面的缺陷,那就是function函数在作为对象的属性,被当作实参传入的时候,丢弃了this,
导致this指向不明
javascript
var foo = {
id:"fooname",
getId: function () {
console.log(this.id)
}
}
setInterval(foo.getId, 1000 / 30);//undefined
由于foo.getId并不是调用,而是传入函数的引用,真正调用的时候是没有对象.getId的,所以没有this,箭头函数独特的特性能够解决这一问题(继承定义时所处的作用域的this)
javascript
var foo = {
id:"fooname",
getId: function () {
setInterval(()=>{
console.log(this.getId);
}, 1000/ 60);
}
}
foo.getId();
上述问题使用bind也可以解决
javascript
var foo = {
id: 'foo',
getId: function () {
setInterval(function () {
console.log(this.id);
}.bind(this), 1000 / 60)
}
}
foo.getId();
这里引出了一个问题,箭头函数把this词法化了,使用箭头函数,它的this类似于在定义时确定下来了,而this是类动态作用域的,这是两种不同的风格,而且箭头函数不能直接定义在foo的属性上,因为箭头函数会继承其所在作用域的this,对象只是一个值,没有形成自己的作用域,所以会出现一种匪夷所思的情况
javascript
var id = 0;
var foo = {
id: 3,
getId: () => {
console.log(this.id);
}
}
foo.getId();//打印0!!