front-end-interview-handbook的JS问题

使用《Javascript高级程序设计》的章节目录对front-end-interview-handbook项目的javascript问题进行了系统的划分整理,试着回答,日后可以考虑做扩展。

第3章 基本概念

nullundefined和未声明变量之间有什么区别?如何检查判断这些状态值?

当没有使用var,let,const等申明变量,就为一个变量进行赋值,该变量就是未声明的变量。未声明的变量会当做在全局作用域下定义的变量。在严格模式下,给未声明的变量进行赋值,会抛出ReferenceError错误。和使用全局变量一样,应尽量避免使用.

1
2
3
4
5
6
7
"use strict"
function foo() {
x = 1; // 在严格模式下,抛出 ReferenceError:x is not defined 错误
}

foo();
console.log(x); // 1

当一个变量已经被声明,却没有进行赋值操作,该变量值为undefined。如果一个函数的执行结果被赋值给一个变量,但是却没有任何返回值,那么该变量的值为undefined。要检查它,需要使用严格相等或是typeof,它会返回undefined字符串。但是不要使用非严格相等来检查,因为如果变量的值为null,使用非严格相等也会返回true;

1
2
3
4
5
6
7
8
9
10
var foo;
console.log(foo); // undefined
console.log(foo === undefined); // true
console.log(typeof foo === 'undefined'); // true

console.log(foo == null); // true. 错误,不要使用非严格相等!

function bar() {}
var baz = bar();
console.log(baz); // undefined

null只能显式的被赋值给变量,表示空值。要检查判断null的值,也要使用严格相等来判断。在这里不能使用非严格相等来判断,因为如果变量值为undefined,使用非严格相等也返回true;

1
2
3
4
var foo = null;
console.log(foo === null); // true

console.log(foo == undefined); // true. 错误,不要使用非严格相等!

不要使用未声明的变量,定义暂时没有值的变量,使用null来为它们赋值。

=====的区别是什么?

==是抽象相等运算符,而===是严格相等运算符。

==运算符是在进行必要的类型转换后,再比较。===运算符不会进行类型转换,所以如果两个值不是相同的类型,会直接返回false。使用==时,可能发生一些特别的事情,例如:

1
2
3
4
5
6
1 == '1'; // true
1 == [1]; // true
1 == true; // true
0 == ''; // true
0 == '0'; // true
0 == false; // true

建议是从不使用==运算符,除了方便与null或undefined比较时,a == null如果a为null或undefined将返回true。

1
2
3
var a = null;
console.log(a == null); // true
console.log(a == undefined); // true

使用letvarconst创建变量有什么区别?

用var声明的变量的作用域是它当前的执行上下文,它可以是嵌套的函数,也可以是声明在任何函数外的变量。let和const是块级作用域,意味着它们只能在最近的一组花括号(function、if-else 代码块或 for 循环中)中访问。var可以变量声明提升,let和const不会进行变量声明提升。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo() {
// 所有变量在函数中都可访问
var bar = 'bar';
let baz = 'baz';
const qux = 'qux';

console.log(bar); // bar
console.log(baz); // baz
console.log(qux); // qux
}

console.log(bar); // ReferenceError: bar is not defined
console.log(baz); // ReferenceError: baz is not defined
console.log(qux); // ReferenceError: qux is not defined
1
2
3
4
5
6
7
8
9
10
11
if (true) {
var bar = 'bar';
let baz = 'baz';
const qux = 'qux';
}

// 用 var 声明的变量在函数作用域上都可访问
console.log(bar); // bar
// let 和 const 定义的变量在它们被定义的语句块之外不可访问
console.log(baz); // ReferenceError: baz is not defined
console.log(qux); // ReferenceError: qux is not defined

var会使变量提升,这意味着变量可以在声明之前使用。let和const不会使变量提升,提前使用会报错。

1
2
3
4
5
6
7
8
9
10
11
console.log(foo); // undefined

var foo = 'foo';

console.log(baz); // ReferenceError: can't access lexical declaration 'baz' before initialization

let baz = 'baz';

console.log(bar); // ReferenceError: can't access lexical declaration 'bar' before initialization

const bar = 'bar';

用var重复声明不会报错,但let和const会。

1
2
3
4
5
6
var foo = 'foo';
var foo = 'bar';
console.log(foo); // "bar"

let baz = 'baz';
let baz = 'qux'; // Uncaught SyntaxError: Identifier 'baz' has already been declared

let和const的区别在于:let允许多次赋值,而const只允许一次。

1
2
3
4
5
6
7
// 这样不会报错。
let foo = 'foo';
foo = 'bar';

// 这样会报错。
const baz = 'baz';
baz = 'qux';

为什么不要使用全局作用域?

每个脚本都可以访问全局作用域,如果人人都使用全局命名空间来定义自己的变量,肯定会发生冲突。使用模块模式(IIFE)将变量封装在本地命名空间中。

什么是”use strict”;?使用它有什么优缺点?

‘use strict’ 是用于对整个脚本或单个函数启用严格模式的语句。严格模式是可选择的一个限制 JavaScript 的变体一种方式 。

优点:

  • 无法再意外创建全局变量。
  • 会使引起静默失败(silently fail,即:不报错也没有任何效果)的赋值操抛出异常。
  • 试图删除不可删除的属性时会抛出异常(之前这种操作不会产生任何效果)。
  • 要求函数的参数名唯一。
  • 全局作用域下,this的值为undefined。
  • 捕获了一些常见的编码错误,并抛出异常。
  • 禁用令人困惑或欠佳的功能。

缺点:

  • 缺失许多开发人员已经习惯的功能。
  • 无法访问function.caller和function.arguments。
  • 以不同严格模式编写的脚本合并后可能导致问题。

参考

第4章 变量,作用域和内存问题

请解释变量提升(hosting)

变量提升(hoisting)是用于解释代码中变量声明行为的术语。在js代码执行的时候,会把使用var 定义的还有function a(){}声明式函数提升到前面,然后依次根据代码的执行顺序,函数声明后全局范围作用域内可以调用使用,而变量声明需要等到赋值完毕才会有值。

第5章 引用类型

请说明.forEach循环和.map()循环的主要区别,它们分别在什么情况下使用?

forEach没有返回值,不改变原数组;
map方法返回更改后的原数组

宿主对象(host objects)和原生对象(native objects)的区别是什么?

范围不同,宿主对象是有宿主提供的对象,理解为js的运行环境衍生出来的对象,如浏览器的window对象,Bom及Dom对象

原生对象是js语法里面涵盖的对象,Object、Function、Array、String、Boolean、Number、Date、RegExp、Error、EvalError、RangeError、ReferenceError、SyntaxError、TypeError、URIError、Global

第6章 面向对象的程序设计

请解释原型继承(prototypal inheritance)的工作原理

在function,Object,Array,String等的数据类型上有一些功能型的原型方法,当这些功能型的方法不是必须的,而且如果每new一个对象上都会绑定这些方法从而引发内存的消耗,这时候我们把方法定义到原型上面,透过原型链来进行访问使用,在这些定义的原型方法上面,我们都保持着同一个指针时的调用路径,不会再开辟内存。

image

第7章 函数表达式

请简述JavaScript中的this

什么是闭包(closure),为什么使用闭包?

闭包是函数和声明该函数的词法环境的组合。可以使在内部定义的函数方法使用父函数中的声明的变量继续操作。

为什么使用闭包?

用闭包模拟私有方法,使用闭包来定义公共函数,并令其可以访问私有函数和变量,也称为 模块模式;

参考

匿名函数的典型应用场景是什么?

匿名函数用作立即执行函数

匿名函数可以用于函数式编程或 Lodash(类似于回调函数)。

1
2
3
4
5
const arr = [1, 2, 3];
const double = arr.map(function(el) {
return el * 2;
});
console.log(double); // [2, 4, 6]

你如何组织自己的代码?使用模块模式(module pattern)还是经典继承(classical inheritance)?

js的继承模式,经典继承大概是构造函数继承

使用模块模式,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var application = function () {

//private variables and functions
var components = new Array();

//initialization
components.push(new BaseComponent());

//create a local copy of application
var app = new BaseComponent();

//public interface
app.getComponentCount = function () {
return components.length;
};

app.registerComponent = function (component) {
if (typeof component == "object") {
components.push(component);
}
};

//return it
return app;
}();

下列语句有什么区别:function Person(){}var person = Person()var person = new Person()

var person = Person()是把函数当做普通函数来调用

var person = new Person()使用new操作符,创建Person对象的实例,该实例继承自Person.prototype。另外一种方式是使用Object.create,例如:Object.create(Person.prototype)`。

1
2
3
4
5
6
7
8
9
10
11
function Person(name) {
this.name = name;
}

var person = Person('John');
console.log(person); // undefined
console.log(person.name); // Uncaught TypeError: Cannot read property 'name' of undefined

var person = new Person('John');
console.log(person); // Person { name: "John" }
console.log(person.name); // "john"

.call.apply有什么区别?

.call和.apply都用于调用函数,第一个参数将用作函数内 this 的值。然而,.call接受逗号分隔的参数作为后面的参数,而.apply接受一个参数数组作为后面的参数。

一个简单的记忆方法是,从call中的 C 联想到逗号分隔(comma-separated),从apply中的 A 联想到数组(array)。

1
2
3
4
5
6
function add(a, b) {
return a + b;
}

console.log(add.call(null, 1, 2)); // 3
console.log(add.apply(null, [1, 2])); // 3

请说明Function.prototype.bind的用法

bind()方法创建一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var module = {
x: 42,
getX: function () {
return this.x;
}
}

var retrieveX = module.getX;
console.log(retrieveX()); // 获取全局范围的变量
// expected output: undefined

var boundGetX = retrieveX.bind(module);
console.log(boundGetX());
// expected output: 42
参考

同步函数按照流程堵塞执行

异步函数通常接收回调函数作为参数,在异步调用函数之后立即执行下一行,不堵塞执行,此时将异步调用的函数放入到事件队列里面,等到事件队列里面空闲的时候就以放入堆栈执行。

请解释function foo() {}var foo = function() {}之间foo的用法上的区别。

第10章 DOM

“attribute” 和 “property” 之间有什么区别?

1
2
3
const input = document.querySelector('input');
console.log(input.getAttribute('value')); // Hello
console.log(input.value); // Hello

但是在文本框中键入“ World!”后:

1
2
console.log(input.getAttribute('value')); // Hello
console.log(input.value); // Hello World!

参考

document 中的load事件和DOMContentLoaded事件之间的区别是什么?

当初始的 HTML 文档被完全加载和解析完成之后,DOMContentLoaded事件被触发,而无需等待样式表、图像和子框架的完成加载。

window的load事件仅在 DOM 和所有相关资源全部完成加载后才会触发。

参考

为什么要使用load事件?这个事件有什么缺点吗?你知道一些代替方案吗,为什么使用它们?

在文档装载完成后会触发load事件。此时,在文档中的所有对象都在 DOM 中,所有图像、脚本、链接和子框架都完成了加载。

DOM 事件DOMContentLoaded将在页面的 DOM 构建完成后触发,但不要等待其他资源完成加载。如果在初始化之前不需要装入整个页面,这个事件是使用首选。

第13章 事件

请解释事件委托(event delegation)

为父元素设置监听器,当子元素触发时,事件会冒泡到父元素上,监听器就会触发。好处:

  • 内存占用减少,因为只需要一个父元素的事件处理程序,而不必为每个后代都添加事件处理程序。
  • 无需从已删除的元素中解绑处理程序,也无需将处理程序绑定到新元素上。

请描述事件冒泡

当一个事件在 DOM 元素上触发时,如果有事件监听器,它将尝试处理该事件,然后事件冒泡到其父级元素,并发生同样的事情。最后直到事件到达祖先元素。事件冒泡是实现事件委托的原理(event delegation)。

什么是事件循环?调用堆栈和任务队列之间有什么区别?

事件循环是一个单线程循环,用于监视调用堆栈并检查是否有工作即将在任务队列中完成。如果调用堆栈为空并且任务队列中有回调函数,则将回调函数出队并推送到调用堆栈中执行。

参考

第21章 Ajax与Comet

请解释关于 JavaScript 的同源策略

同源策略可防止 JavaScript 发起跨域请求。源被定义为 URI、主机名和端口号的组合。此策略可防止页面上的恶意脚本通过该页面的文档对象模型,访问另一个网页上的敏感数据。

参考

  • https://en.wikipedia.org/wiki/Same-origin_policy

    请尽可能详细地解释 Ajax

    使用Ajax的优缺点分别是什么?

    请说明 JSONP 的工作原理,它为什么不是真正的 Ajax?

    JSONP 通过