下面函数x输出?
function test() {
var x = 'var', result = [];
result.push(x);
try {
throw 'exception';
}catch (x) {
x = 'catch';
}
result.push(x);
return result;
}
// Javascript没有块级作用域的一个例外恰好是其异常处理。
test(); // ['var', 'var']
参考
- Effective Javascript 第12条:理解变量声明提升
闭包的三个基本事实
- Javascript允许你引用在当前函数以外定义的变量。
- 即使外部函数已经返回,当前函数仍然可以引用在外部函数所定义的变量。
- 闭包可以更新外部变量的值。
参考
- Effective Javascript 第11条:熟练掌握闭包
请解释事件委托(event delegation)
事件委托是将事件监听器添加到父元素,而不是每个子元素单独设置监听。当触发子元素时,事件会冒泡到父元素,监听器就会触发。 这种技术的好处是:
- 减少内存的占用,只需要一个父元素的事件处理程序,而不必为每个后代添加事件处理程序
- 无需从已删除的元素中解绑处理程序,也无需将处理程序绑定到新元素上
参考
- https://davidwalsh.name/event-delegate
- https://stackoverflow.com/questions/1687296/what-is-dom-event-delegation
请简述Javascript
中的this
JS中的this
是一个相对复杂的概念,不是简单几句能解释清楚的。粗略的讲,函数的调用方式决定了this的值。我阅读了网上很多关于this
的文章Arnav Aggrawal写的比较清楚。this
取值符合以下规则:
- 在调用函数时使用
new
关键字,函数内的this是一个全新的对象。 - 如果
apply
、call
或bind
方法调用、创建一个函数,函数内的this就是作为参数传入这些方法的对象。 - 当函数作为对象里的方法被调用时,函数内的
this
是调用该函数的对象。比如:当obj.method()被调用时,函数内的this将绑定到obj对象 - 如果调用函数不符合上述规则,那么
this
的值指向全局对象(global object)。浏览器环境下指向window
对象,但在严格模式下(use strict
),this
的值为undefined
。 - 如果符合上述多个规则,则较高的规则(1最高,4最低)将决定
this
值。 - 如果该函数是ES2015中的箭头函数,将忽略上面所有规则,
this
将被设置为它被创建时的上下文。 想获得更深入的解释,请查看他在 Medium 上的文章。
参考
- https://codeburst.io/the-simple-rules-to-this-in-javascript-35d97f31bde3
- https://stackoverflow.com/a/3127440/1751946
请解释原型继承(prototypal inheritance)的工作原理
这是一个非常常见的Javascript问题。所有JS对象都有一个__proto__属性,指向它的原型对象。当试图访问一个对象的属性时,如果没有在该对象上找到,它会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或达到原型的末尾(Object.prototype)。这种行为是在模拟经典的继承,但是与其说是继承,不如说是委托(delegation)
参考
- https://www.quora.com/What-is-prototypal-inheritance/answer/Kyle-Simpson
- https://davidwalsh.name/javascript-objects
说说你对AMD
和CommonJS
的了解
存疑
它们都是实现模块体系的方式,直到ES2015出现之前,Javascript一直没有模块体系。CommonJS
是同步的,而AMD
(Asynchronous Module Definitions)从全称中可以明显看出是异步的。CommonJS
的设计是为服务器端开发考虑的,而AMD
支持异步加载模块,更适合浏览器。
我发现AMD
的语法非常冗长,CommonJS
更接近其他语言import
声明语句用法习惯。大多数情况下,我认为AMD
没有使用的必要,因为如果把所有Javascript
都捆绑进一个文件中,将无法得到异步加载的好处。此外,CommonJS
语法上更接近Node
编写模块的风格,在前后端都使用Jaascript
开发之间进行切换时,语境的切换开销较小。
我很高兴看到ES2015
的模块加载方案同时支持同步和异步,我们终于可以只使用一种方案了。虽然它尚未在浏览器和Node
中完全推出,但是我们可以使用代码转换工具进行转换。
参考
- https://exploringjs.com/es6/ch_modules.html
- https://segmentfault.com/q/1010000005680390
- https://auth0.com/blog/javascript-module-systems-showdown/
- https://stackoverflow.com/questions/16521471/relation-between-commonjs-amd-and-requirejs
请解释下面代码为什么不能用作IIFE
:function foo(){ }();
,需要作出哪些修改才能使其成为IIFE
?
IIFE
(Immediately Involved Function Expressions)代表立即执行函数。Javascript
解析器将function foo(){ }();
解析成function foo(){ }
和();
。其中前者是函数声明;后者是一对括号试图调用一个函数,却没有指定名称,因此它会抛出Uncaught SyntaxError: Unexpected token
的错误。
修改方法是:再添加一对括号,形式上有两种:(function foo(){ })()
和(function foo(){ }())
。以上函数不会暴露到全局作用域,如果不需要在函数内部引用自身,可以省略函数的名称。
你可能会用到void
操作符:void function foo(){ }();
。但是,这种做法是有问题的。表达式的值是undefined
,所以如果你的IIFE
有返回值,不要用这种做法。例如:
const foo = void(function bar(){
return 'foo';
})();
console.log(foo); // undefined
注:
function
之前的左圆括号是必需的,因为如果不写这个左圆括号,Javascript
解释器会试图将关键字function
解析为函数声明语句。使用圆括号Javascript
解释器才会正确地将其解析为函数定义表达式。使用圆括号是习惯用法,尽管有时候没有必要也不应当省略。《Javascript权威指南 第六版》 8-5节 181 页
参考
- http://lucybain.com/blog/2014/immediately-invoked-function-expression/
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/void
null
、undefined
和未声明的变量之间有什么区别?如何检查判断这些状态值?
当你没有提前使用var
、let
或const
声明变量,就为一个变量赋值时,该变量是未声明变量(undeclared variables)。未声明变量会脱离当前作用域,成为全局作用域下定义的变量。在严格模式下,给未声明的变量赋值,会抛出ReferenceError
错误。和使用全局变量一样,使用未声明变量也是非常不好的做法,应当尽可能避免。要检查判断它们,需要将用到它们的代码放在try
/catch
语句中。
function foo() {
x = 1; // 在严格模式下,抛出 ReferenceError 错误
}
foo();
console.log(x); // 1
当一个变量已经声明,但没有赋值时,该变量的值是undefined
。如果一个函数的执行结果被赋值给一个变量,但是这个函数却没有返回任何值,那么该变量的值是undefined
。要检查它,需要使用严格相等(===
);或者使用typeof
,它会返回'undefined'
字符串。请注意,不能使用非严格相等(==
)来检查,因为如果变量值为null
,使用非严格相等也会返回true
。
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
只能被显式赋值给变量。它表示空值
,与被显式赋值 undefined
的意义不同。要检查判断null
值,需要使用严格相等运算符。请注意,和前面一样,不能使用非严格相等(==
)来检查,因为如果变量值为undefined
,使用非严格相等也会返回true
。
var foo = null;
console.log(foo === null); // true
console.log(foo == undefined); // true. 错误,不要使用非严格相等!
参考
- https://stackoverflow.com/questions/15985875/effect-of-declared-and-undeclared-variables
- https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/undefined
什么是闭包(closure),为什么使用闭包?
闭包是函数和声明该函数的词法环境的组合。词法作用域中使用的域,是变量在代码中声明的位置所决定的。闭包是即使被外部函数返回,依然可以访问到外部(封闭)函数作用域的函数。
注:闭包是指有权访问另一个函数作用域中的变量的函数。
《Javascript高级程序设计(第三版)》7-2节 178页
注:有关闭包中的this
。《Javascript高级程序设计(第三版)》7-2-2节 182页
var name = "The Window" var object = { name: "My Object", getNameFunc: function() { return function() { return this.name } } } console.log(object.getNameFunc()()) // "The Window" (在非严格模式下)
var name = "The Window" var object = { name: "My Object", getNameFunc: function() { var that = this return function() { return that.name } } } console.log(object.getNameFunc()()) // "My Object"
为什么使用闭包?
- 利用闭包实现数据私有化或模拟私有方法。这个方式也称为模块模式(module pattern)。
- 部分参数函数(partial applications)柯里化(currying).
参考
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
- https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-closure-b2f0d2152b36
请说明.forEach
循环和.map()
循环的主要区别,它们分别在什么情况下使用?
forEach
- 遍历数组中的元素。
- 为每个元素执行回调。
- 无返回值。
const a = [1, 2, 3]; const doubled = a.forEach((num, index) => { // 执行与 num、index 相关的代码 }); // doubled = undefined
map
- 遍历数组中的元素
- 通过对每个元素调用函数,将每个元素“映射(map)”到一个新元素,从而创建一个新数组。
const a = [1, 2, 3]; const doubled = a.map((num) => { return num * 2; }); // doubled = [2, 4, 6]
.forEach
和.map()
的主要区别在于.map()
返回一个新的数组。如果你想得到一个结果,但不想改变原始数组,用.map()
。如果你只需要在数组上做迭代修改,用forEach
。
参考
匿名函数的典型应用场景是什么?
匿名函数可以在 IIFE 中使用,来封装局部作用域内的代码,以便其声明的变量不会暴露到全局作用域。
(function () {
// 一些代码。
})();
匿名函数可以作为只用一次,不需要在其他地方使用的回调函数。当处理函数在调用它们的程序内部被定义时,代码具有更好地自闭性和可读性,可以省去寻找该处理函数的函数体位置的麻烦。
setTimeout(function () {
console.log('Hello world!');
}, 1000);
匿名函数可以用于函数式编程或 Lodash(类似于回调函数)。
const arr = [1, 2, 3];
const double = arr.map(function (el) {
return el * 2;
});
console.log(double); // [2, 4, 6]
参考
- https://www.quora.com/What-is-a-typical-usecase-for-anonymous-functions
- https://stackoverflow.com/questions/10273185/what-are-the-benefits-to-using-anonymous-functions-instead-of-named-functions-fo
宿主对象(host objects)和原生对象(native objects)的区别是什么?
原生对象是由 ECMAScript 规范定义的 JavaScript 内置对象,比如String
、Math
、RegExp
、Object
、Function
等等。
宿主对象是由运行时环境(浏览器或 Node)提供,比如window
、XMLHTTPRequest
等等。
参考
下列语句有什么区别:function Person(){}
、var person = Person()
和var person = new Person()
?
这个问题问得很含糊。我猜这是在考察 JavaScript 中的构造函数(constructor)。从技术上讲,function Person(){}
只是一个普通的函数声明。使用 PascalCase 方式命名函数作为构造函数,是一个惯例。
注:函数声明,首字母大写按惯例当作构造函数使用,会变量提升。
var person = Person()
将Person
以普通函数调用,而不是构造函数。如果该函数是用作构造函数的,那么这种调用方式是一种常见错误。通常情况下,构造函数不会返回任何东西,因此,像普通函数一样调用构造函数,只会返回undefined
赋给用作实例的变量。
注:函数定义表达式,首字母大写按惯例当作构造函数使用,不会变量提升。
var person = new Person()
使用new
操作符,创建Person
对象的实例,该实例继承自Person.prototype
。另外一种方式是使用Object.create
,例如:Object.create(Person.prototype)`。
注:构造函数的使用
参考
.call
和.apply
有什么区别?
.call
和.apply
都用于调用函数,第一个参数将用作函数内 this 的值。然而,.call
接受逗号分隔的参数作为后面的参数,而.apply
接受一个参数数组作为后面的参数。一个简单的记忆方法是,从call
中的 C 联想到逗号分隔(comma-separated),从apply
中的 A 联想到数组(array)。
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
的用法。
摘自MDN:
bind()
方法创建一个新的函数, 当被调用时,将其 this 关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。 注:《Javascript权威指南 第六版》 8-7-4节 190页
《Javascript高级程序设计 第三版》 22-1-4节 602页
function f(y) {return this.x + y;} var o = {x: 1} var g = f.bind(o) g(2) // => 3
var sum = function(x, y) {return x + y;} var succ = sum.bind(null, 1); succ(2) // => 3
function f(y,z){return this.x + y + z;} var g = f.bind({x:1}, 2) g(3) // => 6
参考
什么时候会用到document.write()
?
document.write()
用来将一串文本写入由document.open()
打开的文档流中。当页面加载后执行document.write()
时,它将调用document.open
,会清除整个文档(<head>
和<body>
会被移除!),并将文档内容替换成给定的字符串参数。因此它通常被认为是危险的并且容易被误用。
注:当页面加载完成后,执行document.write()会重写整个页面内容,其它还有writeIn()、open()、close()方法,具体参见
《Javascript高级程序设计 第三版》10-1-2-6节 259页
网上有一些答案,解释了document.write()
被用于分析代码中,或者当你想包含只有在启用了 JavaScript 的情况下才能工作的样式。它甚至在 HTML5 样板代码中用于并行加载脚本并保持执行顺序!但是,我怀疑这些使用原因是过时的,现在可以在不使用document.write()
的情况下实现。如果我的观点有错,请纠正我。
参考
- https://www.quirksmode.org/blog/archives/2005/06/three_javascrip_1.html
- https://github.com/h5bp/html5-boilerplate/wiki/Script-Loading-Techniques#documentwrite-script-tag
功能检测(feature detection)、功能推断(feature inference)和使用 UA 字符串之间有什么区别?
功能检测(feature detection) 功能检测包括确定浏览器是否支持某段代码,以及是否运行不同的代码(取决于它是否执行),以便浏览器始终能够正常运行代码功能,而不会在某些浏览器中出现崩溃和错误。例如:
if ('geolocation' in navigator) {
// 可以使用 navigator.geolocation
} else {
// 处理 navigator.geolocation 功能缺失
}
Modernizr是处理功能检测的优秀工具。 功能推断(feature inference) 功能推断与功能检测一样,会对功能可用性进行检查,但是在判断通过后,还会使用其他功能,因为它假设其他功能也可用,例如:
if (document.getElementsByTagName) {
element = document.getElementById(id);
}
非常不推荐这种方式。功能检测更能保证万无一失。 UA 字符串 这是一个浏览器报告的字符串,它允许网络协议对等方(network protocol peers)识别请求用户代理的应用类型、操作系统、应用供应商和应用版本。它可以通过navigator.userAgent
访问。 然而,这个字符串很难解析并且很可能存在欺骗性。例如,Chrome 会同时作为 Chrome 和 Safari 进行报告。因此,要检测 Safari,除了检查 Safari 字符串,还要检查是否存在 Chrome 字符串。不要使用这种方式。
参考
- https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Feature_detection
- https://stackoverflow.com/questions/20104930/whats-the-difference-between-feature-detection-feature-inference-and-using-th
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
请尽可能详细地解释Ajax
。
Ajax
(asynchronous JavaScript and XML)是使用客户端上的许多Web
技术,创建异步Web
应用的一种Web
开发技术。借助Ajax
,Web
应用可以异步(在后台)向服务器发送数据和从服务器检索数据,而不会干扰现有页面的显示和行为。通过将数据交换层与表示层分离,Ajax
允许网页和扩展Web
应用程序动态更改内容,而无需重新加载整个页面。实际上,现在通常将XML
替换为JSON
,因为JavaScript
对JSON
有原生支持优势。
XMLHttpRequest
API 经常用于异步通信。此外还有最近流行的fetch
API。
参考
使用Ajax
的优缺点分别是什么?
优点
- 交互性更好。来自服务器的新内容可以动态更改,无需重新加载整个页面。
- 减少与服务器的连接,因为脚本和样式只需要被请求一次。
- 状态可以维护在一个页面上。
JavaScript
变量和DOM
状态将得到保持,因为主容器页面未被重新加载。 - 基本上包括大部分
SPA
的优点。 缺点 - 动态网页很难收藏。
- 如果
JavaScript
已在浏览器中被禁用,则不起作用。 - 有些网络爬虫不执行
JavaScript
,也不会看到JavaScript
加载的内容。 - 基本上包括大部分
SPA
的缺点。
请说明JSONP
的工作原理,它为什么不是真正的Ajax
?
JSONP
(带填充的 JSON)是一种通常用于绕过Web
浏览器中的跨域限制的方法,因为Ajax
不允许跨域请求。
JSONP
通过<script>
标签发送跨域请求,通常使用callback
查询参数,例如:https://example.com?callback=printData
。 然后服务器将数据包装在一个名为printData
的函数中并将其返回给客户端。
<!-- https://mydomain.com -->
<script>
function printData(data) {
console.log(`My name is ${data.name}!`);
}
</script>
<script src="https://example.com?callback=printData"></script>
// 文件加载自 https://example.com?callback=printData
printData({name: 'Yang Shun'});
客户端必须在其全局范围内具有printData
函数,并且在收到来自跨域的响应时,该函数将由客户端执行。
JSONP 可能具有一些安全隐患。由于JSONP
是纯JavaScript
实现,它可以完成JavaScript
所能做的一切,因此需要信任JSONP
数据的提供者。
现如今,跨来源资源共享(CORS) 是推荐的主流方式,JSONP 已被视为一种比较 hack 的方式。
参考
请解释变量提升(hoisting)。
变量提升(hoisting)是用于解释代码中变量声明行为的术语。使用var
关键字声明或初始化的变量,会将声明语句“提升”到当前作用域的顶部。 但是,只有声明才会触发提升,赋值语句(如果有的话)将保持原样。我们用几个例子来解释一下。
// 用 var 声明得到提升
console.log(foo); // undefined
var foo = 1;
console.log(foo); // 1
// 用 let/const 声明不会提升
console.log(bar); // ReferenceError: bar is not defined
let bar = 2;
console.log(bar); // 2
函数声明会使函数体提升,但函数表达式(以声明变量的形式书写)只有变量声明会被提升。
// 函数声明
console.log(foo); // [Function: foo]
foo(); // 'FOOOOO'
function foo() {
console.log('FOOOOO');
}
console.log(foo); // [Function: foo]
// 函数表达式
console.log(bar); // undefined
bar(); // Uncaught TypeError: bar is not a function
var bar = function () {
console.log('BARRRR');
};
console.log(bar); // [Function: bar]
请描述事件冒泡。
当一个事件在 DOM 元素上触发时,如果有事件监听器,它将尝试处理该事件,然后事件冒泡到其父级元素,并发生同样的事情。最后直到事件到达祖先元素。事件冒泡是实现事件委托的原理(event delegation)。
attribute
和property
之间有什么区别?
Attribute
是在HTML
中定义的,而property
是在DOM
上定义的。为了说明区别,假设我们在 HTML 中有一个文本框:<input type="text" value="Hello">
。
const input = document.querySelector('input');
console.log(input.getAttribute('value')); // Hello
console.log(input.value); // Hello
但是在文本框中键入 World!
后:
console.log(input.getAttribute('value')); // Hello
console.log(input.value); // Hello World!
参考
为什么扩展JavaScript
内置对象是不好的做法?
扩展JavaScript
内置(原生)对象意味着将属性或方法添加到其prototype
中。虽然听起来很不错,但事实上这样做很危险。想象一下,你的代码使用了一些库,它们通过添加相同的 contains 方法来扩展Array.prototype
,如果这两个方法的行为不相同,那么这些实现将会相互覆盖,你的代码将不能正常运行。
扩展内置对象的唯一使用场景是创建polyfill
,本质上为老版本浏览器缺失的方法提供自己的实现,该方法是由 JavaScript 规范定义的。
参考
document中的
load事件和
DOMContentLoaded`事件之间的区别是什么?
当初始的HTML
文档被完全加载和解析完成之后,DOMContentLoaded
事件被触发,而无需等待样式表、图像和子框架的完成加载。 window
的load
事件仅在DOM
和所有相关资源全部完成加载后才会触发。
参考
- https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded
- https://developer.mozilla.org/en-US/docs/Web/Events/load
==
和===
的区别是什么?
==
是抽象相等运算符,而===
是严格相等运算符。==
运算符是在进行必要的类型转换后,再比较。===
运算符不会进行类型转换,所以如果两个值不是相同的类型,会直接返回false
。使用==
时,可能发生一些特别的事情,例如:
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
。
var a = null;
console.log(a == null); // true
console.log(a == undefined); // true
参考
请解释关于JavaScript
的同源策略。
同源策略可防止JavaScript
发起跨域请求。源被定义为 URI、主机名和端口号的组合。此策略可防止页面上的恶意脚本通过该页面的文档对象模型,访问另一个网页上的敏感数据。
参考
请使下面的语句生效:
duplicate([1, 2, 3, 4, 5]); // [1,2,3,4,5,1,2,3,4,5]
function duplicate(arr) {
return arr.concat(arr);
}
// or
function duplicate(arr) {
return [...arr, ...arr];
}
duplicate([1, 2, 3, 4, 5]); // [1,2,3,4,5,1,2,3,4,5]
什么是"use strict";
?使用它有什么优缺点?
‘use strict’ 是用于对整个脚本或单个函数启用严格模式的语句。严格模式是可选择的一个限制JavaScript
的变体一种方式 。 优点:
- 无法再意外创建全局变量。
- 会使引起静默失败(silently fail,即:不报错也没有任何效果)的赋值操抛出异常。
- 试图删除不可删除的属性时会抛出异常(之前这种操作不会产生任何效果)。
- 要求函数的参数名唯一。
- 全局作用域下,
this
的值为undefined
。 - 捕获了一些常见的编码错误,并抛出异常。
- 禁用令人困惑或欠佳的功能。
缺点:
- 缺失许多开发人员已经习惯的功能。
- 无法访问
function.caller
和function.arguments
。 - 以不同严格模式编写的脚本合并后可能导致问题。
参考
创建一个循环,从 1 迭代到 100,3
的倍数时输出 “fizz”,5
的倍数时输出 “buzz”,同时为3
和5
的倍数时输出 “fizzbuzz”。
来自 Paul Irish的 FizzBuzz。
for (let i = 1; i <= 100; i++) {
let f = i % 3 == 0,
b = i % 5 == 0;
console.log(f ? (b ? 'FizzBuzz' : 'Fizz') : b ? 'Buzz' : i);
}
关于更多千奇百怪的 FizzBuzz 实现,请查看下面的参考链接。
参考
为什么不要使用全局作用域?
每个脚本都可以访问全局作用域,如果人人都使用全局命名空间来定义自己的变量,肯定会发生冲突。使用模块模式(IIFE)将变量封装在本地命名空间中。
为什么要使用load
事件?这个事件有什么缺点吗?你知道一些代替方案吗,为什么使用它们?
在文档装载完成后会触发load
事件。此时,在文档中的所有对象都在 DOM 中,所有图像、脚本、链接和子框架都完成了加载。 DOM 事件DOMContentLoaded
将在页面的 DOM 构建完成后触发,但不要等待其他资源完成加载。如果在初始化之前不需要装入整个页面,这个事件是使用首选。
参考
请解释单页应用是什么,如何使其对SEO
友好。
以下摘自 Grab Front End Guide。 现如今,Web
开发人员将他们构建的产品称为Web
应用,而不是网站。虽然这两个术语之间没有严格的区别,但网络应用往往具有高度的交互性和动态性,允许用户执行操作并接收他们的操作响应。在过去,浏览器从服务器接收HTML
并渲染。当用户导航到其它URL
时,需要整页刷新,服务器会为新页面发送新的HTML
。这被称为服务器端渲染。
然而,在现代的SPA
中,客户端渲染取而代之。浏览器从服务器加载初始页面、整个应用程序所需的脚本(框架、库、应用代码)和样式表。当用户导航到其他页面时,不会触发页面刷新。该页面的URL
通过 HTML5 History API 进行更新。浏览器通过 AJAX 请求向服务器检索新页面所需的数据(通常采用JSON
格式)。然后,SPA
通过JavaScript
来动态更新页面,这些JavaScript
在初始页面加载时已经下载。这种模式类似于原生移动应用的工作方式。
好处:
- 用户感知响应更快,用户切换页面时,不再看到因页面刷新而导致的白屏。
- 对服务器进行的
HTTP
请求减少,因为对于每个页面加载,不必再次下载相同的资源。 - 客户端和服务器之间的关注点分离。可以为不同平台(例如手机、聊天机器人、智能手表)建立新的客户端,而无需修改服务器代码。只要
API
没有修改,可以单独修改客户端和服务器上的代码。
坏处:
- 由于加载了多个页面所需的框架、应用代码和资源,导致初始页面加载时间较长。
- 服务器还需要进行额外的工作,需要将所有请求路由配置到单个入口点,然后由客户端接管路由。
SPA
依赖于JavaScript
来呈现内容,但并非所有搜索引擎都在抓取过程中执行JavaScript
,他们可能会在你的页面上看到空的内容。这无意中损害了应用的搜索引擎优化(SEO)。然而,当你构建应用时,大多数情况下,搜索引擎优化并不是最重要的因素,因为并非所有内容都需要通过搜索引擎进行索引。为了解决这个问题,可以在服务器端渲染你的应用,或者使用诸如 Prerender 的服务来“在浏览器中呈现你的javascript
,保存静态 HTML,并将其返回给爬虫”。
参考
- https://github.com/grab/front-end-guide#single-page-apps-spas
- http://stackoverflow.com/questions/21862054/single-page-app-advantages-and-disadvantages
- http://blog.isquaredsoftware.com/presentations/2016-10-revolution-of-web-dev/
- https://medium.freecodecamp.com/heres-why-client-side-rendering-won-46a349fadb52
Promise
代替回调函数有什么优缺点?
优点:
- 避免可读性极差的回调地狱。
- 使用
.then()
编写的顺序异步代码,既简单又易读。 - 使用
Promise.all()
编写并行异步代码变得很容易。
缺点:
- 轻微地增加了代码的复杂度(这点存在争议)。
- 在不支持 ES2015 的旧版浏览器中,需要引入 polyfill 才能使用。
你使用什么语句遍历对象的属性和数组的元素?
对象:
for
循环:for (var property in obj) { console.log(property); }
。但是,这还会遍历到它的继承属性,在使用之前,你需要加入obj.hasOwnProperty(property)
检查。Object.keys()
:Object.keys(obj).forEach(function (property) { ... })
。Object.keys()
方法会返回一个由一个给定对象的自身可枚举属性组成的数组。Object.getOwnPropertyNames()
:Object.getOwnPropertyNames(obj).forEach(function (property) { ... })
。Object.getOwnPropertyNames()
方法返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括 Symbol 值作为名称的属性)组成的数组。
数组:
for
loops:for (var i = 0; i < arr.length; i++)
。这里的常见错误是var
是函数作用域而不是块级作用域,大多数时候你想要迭代变量在块级作用域中。ES2015 引入了具有块级作用域的let
,建议使用它。所以就变成了:for (let i = 0; i < arr.length; i++)
。forEach
:arr.forEach(function (el, index) { ... })
。这个语句结构有时会更精简,因为如果你所需要的只是数组元素,你不必使用index
。还有every
和some
方法可以让你提前终止遍历。
注:还有ES6中的Set集合与Map集合未考虑
《深入理解ES6》 美 NICHOLAS C.ZAKAS 著 7章 128页
请解释可变对象和不可变对象之间的区别。
- 在
JavaScript
中,string
和number
从设计之初就是不可变(Immutable)。 - _不可变_ 其实是保持一个对象状态不变,这样做的好处是使得开发更加简单,可回溯,测试友好,减少了任何可能的副作用。但是,每当你想添加点东西到一个不可变(Immutable)对象里时,它一定是先拷贝已存在的值到新实例里,然后再给新实例添加内容,最后返回新实例。相比可变对象,这势必会有更多内存、计算量消耗。
const student1 = {
school: 'Baidu',
name: 'HOU Ce',
birthdate: '1995-12-15',
};
const changeStudent = (student, newName, newBday) => {
return {
...student, // 使用解构
name: newName, // 覆盖name属性
birthdate: newBday, // 覆盖birthdate属性
};
};
const student2 = changeStudent(student1, 'YAN Haijing', '1990-11-10'); // both students will have the name properties
console.log(student1, student2); //Object {school: "Baidu", name: "HOU Ce", birthdate: "1995-12-15"} Object {school: "Baidu", name: "YAN Haijing", birthdate: "1990-11-10"}
参考
- https://juejin.im/post/58d0ff6f1b69e6006b8fd4e9
- https://www.interviewcake.com/concept/java/mutable
- https://www.sitepoint.com/immutability-javascript/
请解释同步和异步函数之间的区别。
同步函数阻塞,而异步函数不阻塞。在同步函数中,语句完成后,下一句才执行。在这种情况下,程序可以按照语句的顺序进行精确评估,如果其中一个语句需要很长时间,程序的执行会停滞很长时间。
异步函数通常接受回调作为参数,在调用异步函数后立即继续执行下一行。回调函数仅在异步操作完成且调用堆栈为空时调用。诸如从 Web 服务器加载数据或查询数据库等重负载操作应该异步完成,以便主线程可以继续执行其他操作,而不会出现一直阻塞,直到费时操作完成的情况(在浏览器中,界面会卡住)。
什么是事件循环?调用堆栈和任务队列之间有什么区别?
事件循环是一个单线程循环,用于监视调用堆栈并检查是否有工作即将在任务队列中完成。如果调用堆栈为空并且任务队列中有回调函数,则将回调函数出队并推送到调用堆栈中执行。
注:图形化应该会更好理解。正好
《Learning TypeScript中文版》 5节 136页
有更详细的说明。
如果你没有看过 Philip Robert 关于事件循环的演讲,你应该看一下。这是观看次数最多的 JavaScript 相关视频之一。
参考
- https://2014.jsconf.eu/speakers/philip-roberts-what-the-heck-is-the-event-loop-anyway.html
- http://theproactiveprogrammer.com/javascript/the-javascript-event-loop-a-stack-and-a-queue/
请解释function foo() {}
和var foo = function() {}
之间foo
的用法上的区别。
前者是函数声明,后者是函数表达式。关键的区别在于函数声明会使函数体提升(具有与变量相同的提升行为),但函数表达式的函数体不能。有关变量提升的更多解释,请参阅上面关于变量提升的问题。如果你试图在定义函数表达式之前调用它,你会得到一个Uncaught TypeError: XXX is not a function
的错误。
函数声明
foo(); // 'FOOOOO'
function foo() {
console.log('FOOOOO');
}
函数表达式
foo(); // Uncaught TypeError: foo is not a function
var foo = function () {
console.log('FOOOOO');
};
参考
使用let
、var
和const
创建变量有什么区别?
用var
声明的变量的作用域是它当前的执行上下文,它可以是嵌套的函数,也可以是声明在任何函数外的变量。let
和const
是块级作用域,意味着它们只能在最近的一组花括号(function、if-else 代码块或 for 循环中)中访问。
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
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
不会使变量提升,提前使用会报错。
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
会。
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
只允许一次。
// 这样不会报错。
let foo = 'foo';
foo = 'bar';
// 这样会报错。
const baz = 'baz';
baz = 'qux';
参考
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const
ES6 的类和 ES5 的构造函数有什么区别?
看一个例子:
// ES5 构造函数
function Person(name) {
this.name = name;
}
// ES6 类
class Person {
constructor(name) {
this.name = name;
}
}
对于简单的构造函数而言,他们看起来很相似。 他们的主要区别体现在类继承上。如果我们想要创建一个继承于 Person
父类的 Student
子类,并且添加一个 studentId
字段,我们需要做的修改如下:
// ES5 构造函数
function Student(name, studentId) {
// 调用父类的构造函数来初始化父类的成员变量
Person.call(this, name);
// 初始化子类自己的成员变量
this.studentId = studentId;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
// ES6 类
class Student extends Person {
constructor(name, studentId) {
super(name);
this.studentId = studentId;
}
}
从上面的例子我们可以看出来,使用 ES5 构造函数来实现继承特别麻烦,而使用 ES6 类的方式来实现就特别容易理解和记忆。
参考
- https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Inheritance
- https://eli.thegreenplace.net/2013/10/22/classical-inheritance-in-javascript-es5
你能给出一个使用箭头函数的例子吗,箭头函数与其他函数有什么不同?
在构造函数里使用箭头函数的主要优点是它的 this
只与箭头函数创建时的 this
保持一致,并且不会修改。所以,当用构造函数去创建一个新的对象的时候,箭头函数的 this
总是指向新创建的对象。比如,假设我们有一个 Person
构造函数,它接受一个 firstName
参数,并且它有两个方法去调用 console.log
这个 firstName
,一个是正常的函数,而另一个则是箭头函数:
const Person = function (firstName) {
this.firstName = firstName;
this.sayName1 = function () {
console.log(this.firstName);
};
this.sayName2 = () => {
console.log(this.firstName);
};
};
const john = new Person('John');
const dave = new Person('Dave');
john.sayName1(); // John
john.sayName2(); // John
// 普通函数的 this 可以被修改,而箭头函数则不会
john.sayName1.call(dave); // Dave (因为 "this" 现在指向了 dave 对象)
john.sayName2.call(dave); // John
john.sayName1.apply(dave); // Dave (因为 "this" 现在指向了 dave 对象)
john.sayName2.apply(dave); // John
john.sayName1.bind(dave)(); // Dave (因为 "this" 现在指向了 dave 对象)
john.sayName2.bind(dave)(); // John
var sayNameFromWindow1 = john.sayName1;
sayNameFromWindow1(); // undefined (因为 "this" 现在指向了 Window 对象)
var sayNameFromWindow2 = john.sayName2;
sayNameFromWindow2(); // John
这里主要的区别是,正常函数的 this
是可以在执行过程中被改变的,而箭头函数的 this
则会一直保持一致。所以在使用箭头函数的时候,你就不需要担心它的上下文被改变了。
(想看更好的演示以及示例代码,可以打开这篇文章: https://medium.com/@machnicki/handle-events-in-react-with-arrow-functions-ede88184bbb)
高阶函数(higher-order)的定义是什么?
高阶函数是将一个或多个函数作为参数的函数,它用于数据处理,也可能将函数作为返回结果。高阶函数是为了抽象一些重复执行的操作。一个典型的例子是map
,它将一个数组和一个函数作为参数。map
使用这个函数来转换数组中的每个元素,并返回一个包含转换后元素的新数组。JavaScript 中的其他常见示例是forEach
、filter
和reduce
。高阶函数不仅需要操作数组的时候会用到,还有许多函数返回新函数的用例。Function.prototype.bind
就是一个例子。
Map 示例: 假设我们有一个由名字组成的数组,我们需要将每个字符转换为大写字母。
const names = ['irish', 'daisy', 'anna'];
不使用高阶函数的方法是这样:
const transformNamesToUppercase = function (names) {
const results = [];
for (let i = 0; i < names.length; i++) {
results.push(names[i].toUpperCase());
}
return results;
};
transformNamesToUppercase(names); // ['IRISH', 'DAISY', 'ANNA']
使用.map(transformerFn)
使代码更简明
const transformNamesToUppercase = function (names) {
return names.map((name) => name.toUpperCase());
};
transformNamesToUppercase(names); // ['IRISH', 'DAISY', 'ANNA']
参考
- https://medium.com/javascript-scene/higher-order-functions-composing-software-5365cf2cbe99
- https://hackernoon.com/effective-functional-javascript-first-class-and-higher-order-functions-713fde8df50a
- https://eloquentjavascript.net/05_higher_order.html
请给出一个解构(destructuring)对象或数组的例子。
解构是 ES6 中新功能,它提供了一种简洁方便的方法来提取对象或数组的值,并将它们放入不同的变量中。 数组解构
// 变量赋值
const foo = ['one', 'two', 'three'];
const [one, two, three] = foo;
console.log(one); // "one"
console.log(two); // "two"
console.log(three); // "three"
// 变量交换
let a = 1;
let b = 3;
[a, b] = [b, a];
console.log(a); // 3
console.log(b); // 1
对象解构
// 变量赋值
const o = {p: 42, q: true};
const {p, q} = o;
console.log(p); // 42
console.log(q); // true
混合解构
let node = {
type: "Identifier",
name: "foo",
loc: {
start: {
line: 1,
column: 1
},
end: {
line: 1,
column: 4
}
},
range: [0, 3]
}
let {
loc: { start },
range: [ startIndex ]
} = node
console.log(start.line) // 1
console.log(start.column) // 1
console.log(startIndex) // 0
解构参数
function setCookie(name, value, options) {
options = options || {}
let secure = options.secure,
path = options.path,
domain = options.domain,
expiress = options.expiress;
// 设置cookie
}
// 解构参数变得更清晰
function setCookie(name, value, { secure, path, domain, expiress }) {
// 设置cookie
}
setCookie("type", "js", {
secure: true,
expiress: 60000
})
参考
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment
- https://ponyfoo.com/articles/es6-destructuring-in-depth
ES6的模板字符串为生成字符串提供了很大的灵活性,你可以举个例子吗?
_模板字面量_(Template literals) 是允许嵌入表达式的字符串字面量。你可以使用多行字符串和字符串插值功能。 语法
`string text``string text line 1
string text line 2``string text ${expression} string text`;
tag`string text ${expression} string text`;
示例
console.log(`string text line 1
string text line 2`);
// "string text line 1
// string text line 2"
var a = 5;
var b = 10;
console.log(`Fifteen is ${a + b} and\nnot ${2 * a + b}.`);
// "Fifteen is 15 and
// not 20."
//show函数采用rest参数的写法如下:
let name = '张三',
age = 20,
message = show`我来给大家介绍:${name}的年龄是${age}.`;
function show(stringArr, ...values) {
let output = '';
let index = 0;
for (; index < values.length; index++) {
output += stringArr[index] + values[index];
}
output += stringArr[index];
return output;
}
message; //"我来给大家介绍:张三的年龄是20."
参考
你能举出一个柯里化函数(curry function)的例子吗?它有哪些好处?
柯里化(currying)是一种模式,其中具有多个参数的函数被分解为多个函数,当被串联调用时,将一次一个地累积所有需要的参数。这种技术帮助编写函数式风格的代码,使代码更易读、紧凑。值得注意的是,对于需要被 curry 的函数,它需要从一个函数开始,然后分解成一系列函数,每个函数都需要一个参数。
注:函数柯里化用于创建已经设置好了一个或多个参数的函数。函数柯里化的基本方法和函数绑定是一样的:使用一个闭包返回一个函数。两者的区别在于,当函数被调用时,返回的函数还需要设置一些传入参数。
// 技术上说curriedAdd并非是柯里化函数,但是很好的展示了其概念 function add(num1, num2) { return num1 + num2 } function curriedAdd(num2) { return add(5, num2) } console.log(add(2, 3)) // 5 console.log(curriedAdd(3)) // 8
function curry(fn) {
if (fn.length === 0) {
return fn;
}
function _curried(depth, args) {
return function (newArgument) {
if (depth - 1 === 0) {
return fn(...args, newArgument);
}
return _curried(depth - 1, [...args, newArgument]);
};
}
return _curried(fn.length, []);
}
function add(a, b) {
return a + b;
}
var curriedAdd = curry(add);
var addFive = curriedAdd(5);
var result = [0, 1, 2, 3, 4, 5].map(addFive); // [5, 6, 7, 8, 9, 10]
function curry(fn) {
var args = Array.prototype.slice.call(arguments, 1)
return function() {
var innerArgs = Array.prototype.slice.call(arguments)
var finalArgs = args.concat(innerArgs)
return fn.apply(null, finalArgs)
}
}
function add(num1, num2) {
return num1 + num2
}
var curriedAdd = curry(add, 5)
console.log(curriedAdd(3)) // 8
var curriedAdd = curry(add, 5, 12)
console.log(curriedAdd()) // 17
参考
使用扩展运算符(spread)的好处是什么,它与使用剩余参数语句(rest)有什么区别?
在函数泛型编码时,ES6 的扩展运算符非常有用,因为我们可以轻松创建数组和对象的拷贝,而无需使用Object.create
、slice
或其他函数库。
function putDookieInAnyArray(arr) {
return [...arr, 'dookie'];
}
const result = putDookieInAnyArray(['I', 'really', "don't", 'like']); // ["I", "really", "don't", "like", "dookie"]
const person = {
name: 'Todd',
age: 29,
};
const copyOfTodd = {...person}
ES6 的剩余参数语句提供了一个简写,允许我们将不定数量的参数表示为一个数组。它就像是扩展运算符语法的反面,将数据收集到数组中,而不是解构数组。剩余参数语句在函数参数、数组和对象的解构赋值中有很大作用。
function addFiveToABunchOfNumbers(...numbers) {
return numbers.map((x) => x + 5);
}
const result = addFiveToABunchOfNumbers(4, 5, 6, 7, 8, 9, 10); // [9, 10, 11, 12, 13, 14, 15]
const [a, b, ...rest] = [1, 2, 3, 4]; // a: 1, b: 2, rest: [3, 4]
const {e, f, ...others} = {
e: 1,
f: 2,
g: 3,
h: 4,
}; // e: 1, f: 2, others: { g: 3, h: 4 }
参考
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment
如何在文件之间共用代码?
这取决于执行 JavaScript 的环境。
在客户端(浏览器环境)上,只要变量或函数在全局作用域(window
)中声明,所有脚本都可以引用它们。或者,通过 RequireJS 采用异步模块定义(AMD)以获得更多模块化方法。
在服务器(Node.js)上,常用的方法是使用 CommonJS。每个文件都被视为一个模块,可以通过将它们附加到module.exports
对象来导出变量和函数。
ES2015 定义了一个模块语法,旨在替换 AMD 和 CommonJS。 这最终将在浏览器和 Node 环境中得到支持。
参考
- http://requirejs.org/docs/whyamd.html
- https://nodejs.org/docs/latest/api/modules.html
- http://2ality.com/2014/09/es6-modules-final.html
什么情况下会用到静态类成员?
静态类成员(属性或方法)不绑定到某个类的特定实例,不管哪个实例引用它,都具有相同的值。静态属性通常是配置变量,而静态方法通常是纯粹的实用函数,不依赖于实例的状态。
参考
打印网页标签个数以及标签最多的一组数据
// tag数
new Set(document.getElementsByTagName("*")).size
// 最多的tag
console.table(Object.entries([...document.getElementsByTagName("*")].map(v=>v.nodeName).reduce((obj, v)=>{
obj[v] = obj[v]? obj[v]+1: 1;
return obj
}, {})).sort((a, b)=>b[1]-a[1]).slice(0, 1))
函数的防抖和节流
在前端开发过程中,经常会遇到需要绑定一些持续触发的事件,如resize
、scroll
、mousemove
等等,但有些时候我们并不希望事件持续触发的过程中频繁的去执行函数。一般来讲,防抖和节流是比较好的解决方案。
<!-- 持续触发的情况 -->
<html>
<body style="margin: 0;">
<div id="content" style="height:100%;line-height:500px;text-align:center; color: #fff;background-color:#ccc;font-size:80px;"></div>
</body>
<script>
let num = 1;
let content = document.getElementById('content');
function count() {
content.innerHTML = num++;
};
content.onmousemove = count;
// content.onmousemove = debounce(count, 1000);
// content.onmousemove = throttle(count, 1000);
</script>
</html>
防抖(debounce)
所谓防抖,就是指触发事件后n秒内只能执行一次,如果在n秒内又触发了事件,则会重新计算函数执行时间。
防抖函数分为非立即执行和立即执行
// 非立即执行
// 函数触发事件后不会立即执行,而是在n秒后执行,如果n秒内又触发了事件,则会重新计cou'tent
function debounce(func, wait) {
let timeout;
return function() {
// let context = this;
// let args = arguments;
if(timeout) clearTimeout(timeout);
timeout = setTimeout(()=>{
// func.apply(context, args);
func.apply(this, arguments);
}, wait);
};
}
// 立即执行
// 触发事件后函数立即执行,然后n秒内不触发事件才能继续执行函数
function debounce(func, wait) {
let timeout;
return function() {
if(timeout) clearTimeout(timeout);
let callNow = !timeout;
timeout = setTimeout(()=>{
timeout = null;
}, wait);
if(callNow) func.apply(this, arguments);
};
}
/**
* @desc 函数防抖
* @param func 函数
* @param wait 延迟执行毫秒数
* @param immediate true 表示立即执行,false 表示非立即执行
*/
function debounce(func, wait, immediate) {
let timeout;
return function() {
if(timeout) clearTimeout(timeout);
if(immediate) {
let callNow = !timeout;
timeout = setTimeout(()=>{
timeout = null;
}, wait);
if(callNow) func.applt(this, arguments);
}else {
timeout = setTimeout(()=> {
func.apply(this, arguments);
}, wait);
}
};
}
节流(throttle)
所谓节流,就是指连续触发事件但是在n秒中只执行一次函数。节流会稀释函数执行频率。
对于节流,一般有两种方式可以实现,分别是时间戳版和定时器版。
// 时间戳版
function throttle(func, wait) {
let previous = 0;
return function() {
let now = Date.now();
if( now - previous > wait ) {
func.apply(this, arguments);
previous = now;
}
};
}
// 定时器版
function throttle(func, wait) {
let timeout;
return function() {
if(!timeout) {
timeout = setTimeout(()=>{
timeout = null;
func.apply(this, arguments);
}, wait);
}
};
}
// 时间戳版和定时器版节流函数的区别是,时间戳版的函数触发是在时间段开始的时候,而定时器版函数触发是在时间段内结束的时候。
/**
* @desc 函数节流
* @param func 函数
* @param wait 延迟执行毫秒数
* @param type 1 表时间戳版,2 表定时器版
*/
function throttle(func, wait, type) {
if(type===1) {
let previous = 0;
}else if(type===2) {
let timeout;
}
return function() {
if(type===1) {
let now = Date.now();
if(now-previous>wait) {
func.applt(this, arguments);
previous = now;
}
}else if(type===2) {
if(!timeout) {
timeout = setTimeout(()=>{
timeout = null;
func.apply(this, arguments);
}, wait);
}
}
}
}
其他
写在最后:
- 原作者yangshun
- 部分摘录自《JavaScript高级程序设计》(第3版)
- 部分摘录自《JavaScript权威指南》(第6版)
- 部分摘录自《深入理解ES6》
- 部分摘录自《Learning TypeScript中文版》
- 部分摘录自《Effective Javascript》
- 感谢他们的付出!
文档信息
- 本文作者:Jian Li
- 本文链接:https://ifwechat.com//2020/06/16/javascript-interview/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)