js执行栈

js 在执行代码时可分为全局上下文、函数上下文、eval 上下文,代码执行过程:创建全局上下文(caller),自上而下执行全局上下文;遇到函数时,函数上下文(callee)被 push 到执行栈顶端;开始执行函数中的代码,caller 被挂起;函数执行完后,callee 被移除出执行栈,继续执行全局上下文

声明标识符

let

  • 作用域是块级作用域
  • 不存在变量声明提前
  • 不能重复定义
  • 存在暂时性死区

    暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量

var tmp = 123;

if (true) {
   tmp = 'abc'; // ReferenceError
   let tmp;
}
// 上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp
// 导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。

// 以下死区比较隐蔽
function bar(x = y, y = 2) {
  return [x, y];
}

bar(); // 报错

const

  • 一般用来声明常量,且声明的常量是不允许改变的
  • 块级作用域
  • 存在暂时性死区

var

  • 作用域是函数作用域
  • 存在变量提升

变量提升

明确两点:

  • 函数声明优于变量声明
  • 函数表达式会将变量提升,但是代码在执行的时候才会被赋值
test();
console.log(test); // 输出下面的函数test(),符合第一点
function test() {
  console.log("我是函数");
}
console.log(test);
var test = "我是变量";
console.log(test); // 输出'我是变量',符合第二点
var test = function (params) {
  console.log("我是函数表达式");
};
console.log(test);
test();

this

this 的指向,是在调用函数时根据上下文所动态决定的

  • 默认绑定
  • 隐式绑定(根据调用关系决定)
const foo = {
  bar: 10,
  fn: function () {
    console.log(this);
    console.log(this.bar);
  },
};
var fn1 = foo.fn;
fn1(); // this 指向window,这就是隐式丢失
foo.fn(); // this 指向foo,这也就是隐式绑定,指向最后调用它的对象
  • 显式绑定(call/apply/bind/new 绑定)

new 绑定的优先级比显式 bind 绑定更高。如果构造函数中显式返回一个值,且返回的是一个对象,那么 this 就指向这个返回的对象;如果返回的不是一个对象,那么 this 仍然指向实例

  • 箭头函数
function foo() {
  return (a) => {
    console.log(this.a);
  };
}

const obj1 = {
  a: 2,
};

const obj2 = {
  a: 3,
};

const bar = foo.call(obj1);
console.log(bar.call(obj2)); // 输出2,箭头函数的绑定无法被修改

—>详解 this

call/apply/bind 的区别和实现

三者作用是相同的,都可以改变 this 的指向,区别在于传参不同。call 传的参数是参数列表,相当于...[参数数组],apply 是参数数组,而 bind 是直接改变这个函数的 this 指向并且返回一个新的函数,而不是直接调用立刻执行。

作用域和作用域链

作用域其实可理解为执行上下文中声明的变量和变量的作用范围。

作用域链: 当访问一个变量时,解释器会首先在当前作用域查找,如果没有找到,就去父作用域找,直到找到该变量或者不在父作用域中,这就是作用域链。

闭包

函数嵌套函数时,内层函数调用外层函数变量,并且在全局下可访问,这就是闭包

栈溢出

  • 递归

面试题:有一个函数,参数是一个函数,返回值也是一个函数,返回的函数功能和入参的函数相似,但这个函数只能执行 3 次,再次执行无效,如何实现

function sayHi() {
  console.log("hi");
}

function threeTimes(fn) {
  let times = 0;
  return () => {
    if (times++ < 3) {
      fn();
    }
  };
}

const newFn = threeTimes(sayHi);
newFn();
newFn();
newFn();
newFn();
newFn(); // 后面两次执行都无任何反应

面试题

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i);
  }, 1000);
}
// serTimeout是异步操作,等待外部循环结束后再执行,此时i=6,于是输出一堆66666
// 解决办法1:立即执行函数
for (var i = 1; i <= 5; i++) {
  (function (i) {
    setTimeout(function timer() {
      console.log(i);
    }, 1000);
  })(i);
}
// 解决办法2:使用let
for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i);
  }, 1000);
}
// 每个 let 会和代码块结合起来形成块级作用域