JavaScript学习

编程有面向过程、面向对象和函数式编程,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);

Array方法

原文地址:https://www.cnblogs.com/pengweii/p/14342411.html