编程有面向过程、面向对象和函数式编程,JavaScript可以使用这3种编程思想。
现代JS引擎主要包含调用栈和堆,比如chrome和node.js都使用V8引擎:
JavaScript不是单纯的解释型或编译型语言,而是JIT型。编译是把整个文件翻译成源文件后再执行,可以不立即执行;而解释是翻译一条执行一条,缺点是慢;JIT则是把整个文件翻译完后再立即执行。
一段代码被送入JS引擎会经历分析、编译、执行3个阶段。在分析阶段,代码被划分成有意义的AST(Abstract Syntax Tree),同时会检查语法错误。
接着在编译阶段使用生成的AST翻译成机器码,最后在执行阶段执行机器码,在调用栈中执行。这就完了吗?并没有,现代JS引擎有一种优化策略:第一次生成执行的是没有经过优化的版本,这样只是为了更快地运行起来。在执行过程中,还会再次编译生成优化的机器码来替代之前的机器码。
JavaScript的运行时类似一个盒子,包含了JavaScript运行所需的东西,比如对于浏览器,JavaScript运行时包含JS引擎、Web API以及调用队列。当调用栈为空时,会把调用队列第一个事件入栈,这就构成Event Loop,事件循环是浏览器或Node的一种解决JavaScript单线程运行时不会阻塞的一种机制,也就是异步的原理。
node.js运行时:
异步面试题
下列代码打印的结果是?
setTimeout(() => console.log(1), 0);
console.log(2);
不要误以为第一行在0秒后执行就是立即执行,所以先打印1再打印2。
正确执行是先打印2再打印1.
事件循环与任务队列
一段js代码刚开始执行的时候会有一个匿名的主事件在Callback Queue任务队列中,js引擎会去任务队列中取一个事件来执行(js是单线程的,每次只能处理一个事件),在执行这个事件中,如果里面有异步操作(如DOM、ajax、setTimeout),它就会将其丢给WebAPIs执行,且丢过去后js引擎就不管了,继续执行后面的代码。WebAPI执行完异步操作后,会把回调函数的js代码再放到Callback Queue中(异步任务都是有回调函数的,如onClick)。如果回调函数中还有异步任务,则在js引擎执行中还会丢给WebAPI,如此反复,这就是事件循环。
再看上面的代码,一开始Callback Queue中会有一个主事件,进入js引擎执行,执行到setTimeout时发现这是一个异步任务,于是将其丢给WebAPI来执行,它继续执行后面的代码,WebAPI在0毫秒后就执行完了,接着把回调函数也就是打印1的任务放到任务队列中,但是发现任务队列中还有一个事件没执行完,也就是主事件,等到主事件执行完打印2以后,才轮到执行打印1。
执行上下文
代码被编译后,首先会为顶层代码(不包含函数体内部)创建一个全局的执行上下文(global execution context),然后在global EC中执行顶层代码,再然后是在每个函数被调用是创建该函数的执行上下文中,然后在其中执行函数和等待回调函数。所以函数的执行上下文构成了call stack。
执行上下文里面有什么
主要包含:
- Variable Environment
- Scope chain:能够让函数访问到这个函数外定义的变量,如全局变量。
- this关键字
这些都是在creation phase(在执行之前)创建的。
箭头函数没有argument object和this关键字,他们可以从最近的父函数使用argu object和this。
、
js引擎是如何知道函数调用的顺序以及当前所处哪一个函数的上下文?:Call stack
Scope chain
注意,只有let和const是block-scoped,var是function-scoped,所以下图中if语句中decade属于if block scope
,而millenial属于first() scope
。在严格模式下,函数也是block-scoped。
一个函数可以访问父函数的变量,并不是将父函数的变量复制了过来,而是通过scope chain向上找到的。
Call stack vs Scope chain
函数调用顺序不会影响scope chain。
lexical scoping and dynamic scoping:
In JavaScript, we have lexical scoping, so the rules of where we can access variables are based on exactly where in the code functions and blocks are written.
变量提升(Hoisting)
为什么需要变量提升?这样可以在声明函数之前使用函数,比如相互递归(mutual recursing),也可以让代码变得更可读。
var的变量提升只是引进let和const变量提升的一个副产物(因为历史包袱不能去掉var),因此var的初始值未undefined,容易引起错误和难以发现的bug,因此尽量不要使用var。
暂时死区
为什么需要暂时死区?在变量声明前引用变量是一个不好的行为,应当避免。
注意下面代码中 console.log(Jonas is a ${job}
) 提示的错误是变量未初始化,而不是未定义,因为在执行代码前js引擎已经扫描过一遍代码了,知道job是定义了,只是还未初始化。而 console.log(x) 则是变量未定义。
console.log(me);
console.log(job);
console.log(year);
var me = 'Jonas';
let job = 'teacher';
const year = 1991;
// Functions
console.log(addDecl(2, 3));
console.log(addExpr(2, 3));
console.log(addArrow(2, 3));
function addDecl(a, b) {
return a + b;
}
const addExpr = function (a, b) {
return a + b;
}
const addArrow = (a, b) => a + b;
若将const改成var
// const addExpr = function (a, b) {
return a + b;
}
// const addArrow = (a, b) => a + b;
var addExpr = function (a, b) {
return a + b;
}
var addArrow = (a, b) => a + b;
same as:
console.log(addExpr)值为undefined。
使用var变量提升的bug例子
if (!numProduct) deletedAllProducts();
var numProduct = 10;
function deletedAllProducts() {
console.log('All products deleted!');
}
即使numProduct不为0,依然会调用deletedAllProducts(),因为undefined的值和0一样,也为false。
var和const、let的另一个小区别
var声明变量会在window对象创建一个属性,而const和let不会。
var x = 1;
const y = 2;
let z = 3;
console.log(x === window.x);
console.log(y === window.y);
console.log(z === window.z);
this关键字
- 调用方法时,this指调用方法的那个object
- 普通调用函数时,this在非严格模式下指window对象,在严格模式下为undefined
- 箭头函数没有自己this,箭头函数的this指其所在函数的this
- 事件监听中this指事件处理程序的DOM元素
'use strict'
console.log(this);
const calcAge = function (birthYear) {
console.log(2021 - birthYear);
console.log(this);
}
calcAge(1998);
const calcArrow = birthYear => {
console.log(2021 - birthYear);
console.log(this);
}
calcArrow(1997);
const jonas = {
year: 1998,
calcAge: function () {
console.log(this);
console.log(2021 - this.year);
}
}
jonas.calcAge();
const matila = {
year: 2000,
}
matila.calcAge = jonas.calcAge;
matila.calcAge();
PRIMITIVES VS OBJECTS
let age = 30
: age指向地址0001,值为30;let oldAge = age
: oldAge指向地址0001,值为30;age = 31
: 不是直接将地址0001的值改为31,而是开辟新的内存0002,age指向0002。
primitive类型是存在call stack的执行上下文中的,而object则在heap中。
heap中的修改并不影响call stack,所以const什么的变量只是call stack的值不能改变,而不是heap中的值。
头等函数(first-class function) 和高阶函数(higher-order function)
当一门编程语言的函数可以被当作变量一样用时,则称这门语言拥有头等函数。
在js中,函数是object,object是value,所以函数也是value。作为value,函数可以当做参数传入另一个函数,也可以被另一个函数返回,还可以赋值给变量或属性。作为object,函数可以有方法和属性。
头等函数是一个概念,不是实际的一类函数。
一个函数就可以接收另一个函数作为参数,或者返回值为一个函数,这种函数就称之为高阶函数。作为参数传入的函数称为回调函数(callback function, means not call me now, call me back when something happend, eg. click),
// 闭包
在下面的代码中,首先声明了一个函数secureBooking,然后调用了该函数,将其赋值给booker。passengerCount是secureBooking的变量,属于secureBooking的执行上下文。
因此在执行完const booker = secureBooking()
后,secureBooking的执行上下文出栈,这时glocal的执行上下文按照scope chain应该是无法访问passengerCount的,但是后面两次调用book()函数,依然可以修改passengerCount,这就是因为闭包。
当函数执行时要访问一个自己没有的变量,首先是看闭包,然后才是去看scope chain,也就是说闭包的优先级是高于scope chain的。
一个函数是有权限访问创造这个函数的执行上下文的变量环境的。
闭包就先人和家乡的关系,即使人不在家乡,他和家乡的联系也还是在的。或者说闭包就像一个函数带着的背包,里面有创建这个函数的所有变量环境。
2个例子
// Example 1
let f;
const g = function () {
const a = 23;
f = function () {
console.log(a * 2);
};
};
g();
f();
// Example 2
const boardPassengers = function (n, wait) {
const perGroup = n / 3;
setTimeout(function () {
console.log(`We are now boarding all ${n} passengers`);
console.log(`There are 3 groups, each with ${perGroup} passengers`);
}, wait * 1000);
console.log(`Will start boarding in ${wait} seconds`);
};
const perGroup = 1000;
boardPassengers(180, 3);