快速掌握JavaScript面试基础知识(二)

译者按: 总结了大量 JavaScript 基本知识点,很有用!

本文采用意译,版权归原作者所有



根据StackOverflow 调查, 自 2014 年一来,JavaScript 是最流行的编程语言。当然,这也在情理之中,毕竟 1/3 的开发工作都需要一些 JavaScript 知识。因此,如果你希望在成为一个开发者,你应该学会这门语言。

这篇博客的主要目的是将所有面试中常见的概念总结,方便你快速去了解。(鉴于本文内容过长,方便阅读,将分为三篇博客来翻译, 此为第二部分。第一部分请点击快速掌握 JavaScript 面试基础知识(一))

闭包

闭包由一个函数以及该函数定义是所在的环境组成。我们通过例子来形象解释它。

function sayHi(name) {
var message = `Hi ${name}!`;
function greeting() {
console.log(message);
}
return greeting;
}
var sayHiToJon = sayHi("Jon");
console.log(sayHiToJon); // ƒ() { console.log(message) }
console.log(sayHiToJon()); // 'Hi Jon!'

请理解var sayHiToJon = sayHi('Jon');这行代码的执行过程,sayHi函数执行,首先将message的值计算出来;然后定义了greeting函数,函数中引用了message变量;最后,返回greeting函数。
如果按照 C/Java 语言的思路,sayHiToJon就等价于greeting函数,那么会报错:message 未定义。但是在 JavaScript 中不一样,这里的sayHiToJon函数等于greeting函数以及一个环境,该环境中包含了message。因此,当我们调用sayHiToJon函数,可以成功地将message打印出来。因此,这里的闭包就是greeting函数和一个包含message变量的环境。(备注: 为了便于理解,此段落未按照原文翻译。)

闭包的一个优势在于数据隔离。我们同样用一个例子来说明:

function SpringfieldSchool() {
let staff = ["Seymour Skinner", "Edna Krabappel"];
return {
getStaff: function() {
console.log(staff);
},
addStaff: function(name) {
staff.push(name);
}
};
}

let elementary = SpringfieldSchool();
console.log(elementary); // { getStaff: ƒ, addStaff: ƒ }
console.log(staff); // ReferenceError: staff is not defined
/* Closure allows access to the staff variable */
elementary.getStaff(); // ["Seymour Skinner", "Edna Krabappel"]
elementary.addStaff("Otto Mann");
elementary.getStaff(); // ["Seymour Skinner", "Edna Krabappel", "Otto Mann"]

elementary被创建的时候,SpringfieldSchool已经返回。也就是说staff无法被外部访问。唯一可以访问的方式就是里面的闭包函数getStaffaddStaff

我们来看一个面试题:下面的代码有什么问题,如何修复?

const arr = [10, 12, 15, 21];
for (var i = 0; i < arr.length; i++) {
setTimeout(function() {
console.log(`The value ${arr[i]} is at index: ${i}`);
}, (i + 1) * 1000);
}

上面的代码输出的结果全部都一样:”The value undefined is at index: 4”。因为所有在setTimeout中定义的匿名函数都引用了同一个外部变量i。当匿名函数执行的时候,i的值为 4。

这个问题可以改用IIFE(后面会介绍)方法来解决,通过对每一个匿名函数构建独立的外部作用域来实现。

const arr = [10, 12, 15, 21];
for (var i = 0; i < arr.length; i++) {
(function(j) {
setTimeout(function() {
console.log(`The value ${arr[j]} is at index: ${j}`);
}, j * 1000);
})(i);
}

当然,还有一个方法,使用let来声明i

const arr = [10, 12, 15, 21];
for (let i = 0; i < arr.length; i++) {
setTimeout(function() {
console.log(`The value ${arr[i]} is at index: ${i}`);
}, i * 1000);
}

立即调用的函数表达式(Immediate Invoked Function Expression)(IIFE)

一个 IIFE 是一个函数表达式在定义之后立即被调用。常用在你想对一个新声明的变量创建一个隔离的作用域。
它的格式为: (function(){....})()。前面的大括号用于告诉编译器这里不仅仅是函数定义,后面的大括号用于执行该函数。

var result = [];
for (var i = 0; i < 5; i++) {
result.push(function() {
return i;
});
}
console.log(result[1]()); // 5
console.log(result[3]()); // 5
result = [];
for (var i = 0; i < 5; i++) {
(function() {
var j = i; // copy current value of i
result.push(function() {
return j;
});
})();
}
console.log(result[1]()); // 1
console.log(result[3]()); // 3

使用 IIFE 可以:

  • 为函数绑定私有数据
  • 创建一个新的环境
  • 避免污染全局命名空间

环境(Context)

我们往往容易将环境(Context)和作用域(Scope)搞混,我来简单解释一下:

  • 环境(Context): 由函数如何被调用而决定,往往指this
  • 作用域(Scope): 可访问的变量。

函数调用:call, apply, bind

这三个方法都是为了将 this 绑定到函数,区别在于调用的方式。

  • .call()会立即执行函数,你需要把参数按顺序传入;
  • .apply()会立即执行函数,你需要把所有的参数组合为一个数组传入;

.call().apply()几乎相同。哪个传入参数方便,你就选择哪个。

const Snow = { surename: "Snow" };
const char = {
surename: "Stark",
knows: function(arg, name) {
console.log(`You know ${arg}, ${name} ${this.surename}`);
}
};
char.knows("something", "Bran"); // You know something, Bran Stark
char.knows.call(Snow, "nothing", "Jon"); // You know nothing, Jon Snow
char.knows.apply(Snow, ["nothing", "Jon"]); // You know nothing, Jon Snow

注意:如果你将数组传入call函数,它会认为只有一个参数。

ES6 允许使用新的操作符将数组变换为一个序列。

char.knows.call(Snow, ...["nothing", "Jon"]); // You know nothing, Jon Snow

.bind()返回一个新的函数,以及相应的环境和参数。如果你想该函数稍后调用,那么推荐使用bind
.bind()函数的优点在于它可以记录一个执行环境,对于异步调用和事件驱动的编程很有用。

.bind()传参数的方式和call相同。

const Snow = { surename: "Snow" };
const char = {
surename: "Stark",
knows: function(arg, name) {
console.log(`You know ${arg}, ${name} ${this.surename}`);
}
};
const whoKnowsNothing = char.knows.bind(Snow, "nothing");
whoKnowsNothing("Jon"); // You know nothing, Jon Snow

this 关键字

要理解 JavaScript 中this关键字,特别是它指向谁,有时候相当地复杂。this的值通常由函数的执行环境决定。简单的说,执行环境指函数如何被调用的。this像是一个占位符(placeholder),它指向当方法被调用时,调用对应的方法的对象。

下面有序地列出了判断this指向的规则。如果第一条匹配,那么就不用去检查第二条了。

  • new绑定 - 当使用new关键字调用函数的时候,this指向新构建的对象。

    function Person(name, age) {
    this.name = name;
    this.age =age;
    console.log(this);
    }
    const Rachel = new Person('Rachel', 30); // { age: 30, name: 'Rachel' }
  • 显示绑定(Explicit binding) - 当使用call或则apply的时候,我们显示的传入一个对象参数,该参数会绑定到this。 注意:.bind()函数不一样。用bind定义一个新的函数,但是依然绑定到原来的对象。

    function fn() {
    console.log(this);
    }
    var agent = {id: '007'};
    fn.call(agent); // { id: '007' }
    fn.apply(agent); // { id: '007' }
    var boundFn = fn.bind(agent);
    boundFn(); // { id: '007' }
  • 隐式绑定 - 当一个函数在某个环境下调用(在某个对象里),this指向该对象。也就是说该函数是对象的一个方法。

    var building = {
    floors: 5,
    printThis: function() {
    console.log(this);
    }
    }
    building.printThis(); // { floors: 5, printThis: function() {…} }
  • 默认绑定 - 如果上面所有的规则都不满足,那么this指向全局对象(在浏览器中,就是 window 对象)。当函数没有绑定到某个对象,而单独定义的时候,该函数默认绑定到全局对象。

    function printWindow() {
    console.log(this)
    }
    printWindow(); // window object

注意:下面的情况中,inner函数中的this指向全局。

function Dinosaur(name) {
this.name = name;
var self = this;
inner();
function inner() {
alert(this); // window object — the function has overwritten the 'this' context
console.log(self); // {name: 'Dino'} — referencing the stored value from the outer context
}
}
var myDinosaur = new Dinosaur("Dino");
  • 词法(Lexical) this - 当是使用=>来定义函数时,this指向定义该函数时候外层的this。 备注:大概是和定义的词法(=>)有关,把它称作Lexical this
function Cat(name) {
this.name = name;
console.log(this); // { name: 'Garfield' }
(() => console.log(this))(); // { name: 'Garfield' }
}
var myCat = new Cat("Garfield");

严格(Strict)模式

如果你使用了"use strict"指令,那么 JavaScript 代码会在严格模式下执行。在严格模式下,对于词法分析和错误处理都有特定的规则。在这里我列出它的一些优点:

  • 使得 Debug 更容易:以前会被忽略的错误现在会显示报错,比如赋值给一个不可写的全局变量或则属性;
  • 避免不小心声明了全局变量:赋值给一个未定义的变量会报错;
  • 避免无效使用 delete:尝试去删除变量、函数或则不可删除的属性会抛出错误;
  • 避免重复的属性名和参数值:对象上重复的属性和函数参数会抛出错误(在 ES6 中不再是这样);
  • 使得eval()更加安全:在eval()中定义的变量和函数在外部作用域不可见;
  • “安全”的消除 JavaScript 中 this 的转换:如果this是 null 或则 undefined 不在转换到全局对象。也就是说在浏览器中使用 this 去指向全局对象不再可行。

对于在严格(strict)模式和测试阶段都没有发现的 bug,不妨接入线上实时监控插件Fundebug

关于Fundebug

Fundebug专注于JavaScript、微信小程序、支付宝小程序线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了80亿+错误事件。欢迎大家免费试用

您的用户遇到BUG了吗?

体验Demo 免费使用