let
var
const
相关问题
var
: 只有函数做用域和全局做用域,没有块级做用域,变量提高,在全局定义的话,能够经过 window.定义的变量名
找到,能够重复声明变量。
let
: 有块级做用域,没有变量提高(这里网上不少不一样意见),可是存在暂时性死区,声明一个变量以后不能重复声明。
const
: 和 let
同样,可是 const
声明的变量是常量,不能够再赋值,而且 const
声明变量的时候,必须初始化。node
遇到的题目git
请写出如下 name 的值,而且说明为何es6
var name = 'a';
(function() {
console.log(name);
})();
// 输出 a, 由于在函数内并无声明 name 属性,因此沿着做用域链去查找 name, 在全局环境找到,输出
var name = 'a';
(function(name) {
console.log(name);
name = 'd';
})();
// 输出 undefined ,由于函数有形参 name 至关于在函数体内定义了 var name; 运行函数的时候并无传参,而且在没有赋值以前就输出,因此输出 undefined
var name = 'a';
(function() {
console.log(name);
var name = 'd';
})();
// 输出 undefined, 由于 var name 提早,实际上和下面的题目同样
var name = 'a';
(function() {
var name;
console.log(name);
name = 'd';
})();
// 输出 undefined,没有赋值以前就读取值了
var name = 'a';
(function() {
console.log(name);
let name = 'd';
})();
// ReferenceError: name is not defined,由于 let name 声明以前,name 是一个暂时性死区,没法读取它的值
var name = 'a';
function name() {
return 1;
}
(function() {
console.log(name);
})();
// 函数声明解析先于变量声明,因此 name 首先是一个函数,而后被从新定义成一个 字符串,输出 a
复制代码
esnext
经常使用的新特性github
let
const
箭头函数 class
Promise
async/await
展开运算符 for...of
循环 rest
参数 精简函数 等。开放性题目。面试
cookie
sessionStorage
localStorage
的区别编程
这三个都是本地存储,能够把信息记录在客户端。
cookie
永久储存,能够设置过时时间,存储大小通常在 4K
左右,每次请求会附带在 header
上,会影响性能。
sessionStorage
页面关闭就清理,不能设置过时时间,不会附带在 header
上,存储大小 5M
左右。
localStorage
永久存储,其余和 sessionStorage
同样。 建议: 最好不要用 cookie
做为本地存储了,有能够完美代替的方案。跨域
es5
的 function
和 es6
的箭头函数有什么区别数组
function
内部可使用 arguments
对象, this
指向函数运行时的对象,能够做为构造器使用,能够被 new
,可使用 prototype
属性, 可使用 yield
关键字。浏览器
箭头函数内部没有 arguments
对象,若是想获取参数列表请使用 rest
参数,没有 this
,它只会从本身的做用域链的上一层继承 this
,不能做为构造器使用,因此也不能被 new
,不能使用 prototype
属性, 不能使用 yield
关键字。缓存
实际上通常答 this
和 arguments
这两个不一样就行了。
this
的定义
在函数中,能够引用运行环境中的变量。所以就须要一个机制来让咱们能够在函数体内部获取当前的运行环境,这即是 this
。 所以要明白 this
指向,其实就是要搞清楚 函数的运行环境,说人话就是,谁调用了函数。例如:
obj.fn()
,即是 obj
调用了函数,既函数中的 this === obj
fn()
,这里能够当作 window.fn()
,所以 this === window
但这种机制并不彻底能知足咱们的业务需求,所以提供了三种方式能够手动修改 this
的指向:
call: fn.call(target, 1, 2)
apply: fn.apply(target, [1, 2])
bind: fn.bind(target)(1,2)
箭头函数没有 this
, 它只会从本身的做用域链的上一层继承 this
遇到的题目
function a() {
return () => {
return () => {
console.log(this);
};
};
}
console.log(a()()());
// 输出 window 对象,由于箭头函数没有 this,因此一直延做用域链找到 a 函数定义的地方,this 和 a 函数内部的 this 是同样的,a在全局环境调用,因此是 window
复制代码
.call
.apply
.bind
三者之间的区别
简单点来讲,这三个均可以改变函数运行的做用域,第一个参数都是函数运行须要绑定的 this
, .call
后面的参数列表是函数运行时的参数列表, .apply
后面的参数是一个数组或者类数组,执行函数,.bind
和 .call
同样,后面的参数列表是初始参数,执行后返回一个原函数的拷贝。
.call
根据输入的参数运行函数
fun.call(thisArg, arg1, arg2, ...)
thisArg
在 fun 函数运行时指定的 this
值, 为空的时候指向 window
arg1, arg2, ...
指定的参数列表。
.apply
根据输入的参数运行函数
fun.apply(thisArg, [argsArray])
thisArg
在 fun 函数运行时指定的 this
值, 为空的时候指向 window
argsArray ...
数组或者类数组,函数运行时的参数列表。
.bind
返回一个原函数的拷贝,并拥有指定的 this 值和初始参数。
function.bind(thisArg[, arg1[, arg2[, ...]]])
thisArg
调用绑定函数时做为 this 参数传递给目标函数的值。 若是使用 new 运算符构造绑定函数,则忽略该值。
arg1, arg2, ...
当目标函数被调用时,预先添加到绑定函数的参数列表中的参数。
async/await
的错误处理机制
try...catch
在有 Promise
和 Generator
的条件下,为何还加了 async/await
1.更好的语义,async
和 await
,比起 *
和 yield
,语义更清楚了。
2.async
函数内置执行器,函数调用以后,会自动执行。
3.async
函数的返回值是 Promise
对象。
4.更广的适用性。co
模块约定,yield
命令后面只能是 Thunk
函数或 Promise
对象,而 async
函数的 await
命令后面,能够是 Promise
对象和原始类型的值。
浏览器中 JavaScript
的 eventloop
笔者的语言能力很是弱,我以为我表述的不是很清楚。能够参考这里面的说法,相关题目也能够找到:
常见异步笔试题
发现这个被提问的仍是比较多的,面试了很多公司都有相似的题目。
Common.js
和 ES6
的模块处理的区别
require
支持 动态导入,import
不支持,正在提案 (babel
能够经过插件得到支持)
require
是 同步 导入,import
属于 异步 导入
require
是 值拷贝,导出值变化不会影响导入值;import
指向 内存地址,导入值会随导出值而变化
JS
经常使用的跨越有哪些
img
标签,JSONP
, CORS
, postMessage
思考题,JSONP
除了只能进行 get
请求之外,还有什么缺点,面试被追问过。不提供答案了。
Debounce
手写
防抖和节流问到的都挺多的
// 既然是手写就写最简单的就好
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
fn(...args);
}, delay);
};
}
// 或者面试官说,若是要 ES5 环境下的
function debounce(fn, delay) {
var timer;
return function() {
var args = arguments;
var _this = this;
clearTimeout(timer);
timer = setTimeout(function() {
fn.apply(_this, args);
}, delay);
};
}
复制代码
try...catch
和抛错相关的问题
请写出这段代码的问题,请至少写出两个。
function fn() {
try {
setTimeout(() => {
throw 'error';
}, 0);
} catch (e) {
console.error(e);
}
}
fn();
复制代码
我笔试的时候写出了两个,真写不出第三个是啥
1.直接抛字符型的错误,不用 Error
对象抛出,错误被截获的时候,没有任何堆栈信息。
2.这里的抛错,没有被 catch
,其实很简单的能懂的一个道理,setTimeout
里面函数执行的时候,fn
早执行完了。
斐波那契数列
手写
很简单的递归题目,在红宝书里面有解
function fn(n) {
if (n < 3) {
return 1;
}
return fn(n - 1) + fn(n - 2);
// return arguments.callee(n - 1) + arguments.callee(n - 2);
}
// 面试官:若是不用递归呢?
function fn(n) {
let a = 0;
let b = 1;
for (let i = 0; i < n; i++) {
[a, b] = [b, a + b];
}
return a;
}
复制代码
Throttle
手写
防抖和节流问到的都挺多的,可是不多遇到两个一块儿出的,最多另一个问原理,一个实现。
// 既然是手写就写最简单的就好
function throttle(fn, delay) {
let timer;
return (...args) => {
if (!timer) {
timer = setTimeout(() => {
fn(...args);
timer = null;
}, delay);
}
};
}
// 或者面试官说,若是要 ES5 环境下的
function throttle(fn, delay) {
var timer;
return function() {
var args = arguments;
var _this = this;
if (!timer) {
timer = setTimeout(function() {
fn.apply(_this, args);
timer = null;
}, delay);
}
};
}
复制代码
JS
类型转换问题
对象转基本类型
对象转基本类型的问题,首先会调用对象的 [Symbol.toPrimitive]
,而后到 valueOf
,而后是 toString
。这三个方法均可以重写。
const a = {
valueOf() {
return 0;
},
toString() {
return '1';
},
[Symbol.toPrimitive]() {
return 2;
}
};
1 + a; // => 3
'1' + a; // => '12'
复制代码
四则运算符
除了加法之外,其余运算都会把参与运算的值转换成数字,转换不了就 NaN
,若是是加法的话,运算中其中一方是字符串,也会把运算另一方转成字符串。 +
-
运算符,也能够把值转换成数字,而且运算符等级比四则运算高。
1 + '1'; // '11'
2 * '2'; // 4
[1, 2] + [2, 1]; // '1,22,1'
'a' + +'b'; // -> 'aNaN' for + 'b' = NaN -> 'a' + NaN = 'aNaN'
1 + -[2, 1]; // -> NaN for -[2, 1] = NaN -> 1 + NaN = NaN
复制代码
==
运算符
1.当比较数字和字符串时,字符串会转换成数字值。 JavaScript
尝试将数字字面量转换为数字类型的值。 首先, 一个数学上的值会从数字字面量中衍生出来,而后获得被四舍五入后的数字类型的值。
2.若是其中一个操做数为布尔类型,那么布尔操做数若是为true
,那么会转换为1
,若是为false
,会转换为整数0
,即0
。
3.若是一个对象与数字或字符串相比较,JavaScript
会尝试返回对象的默认值。操做符会尝试经过方法valueOf
和toString
将对象转换为其原始值(一个字符串或数字类型的值)。若是尝试转换失败,会产生一个运行时错误。 4.注意:当且仅当与原始值比较时,对象会被转换为原始值。当两个操做数均为对象时,它们做为对象进行比较,仅当它们引用相同对象时返回true
。
[0] == false
[1] == [1]
[] == ![]
// 试着求结果而且解释为何,真实面试题。
复制代码
数组去重手写
// 最简单
function fn(arr) {
return [...new Set(...arr)];
}
// 每每面试官要的不是这个答案
// 实现方法不少,在不考虑时间复杂度的状况下,怎么写都行,毕竟面向面试编程
复制代码
数组铺平实现
// 实际写代码
function fn(arr) {
return arr.flat(Infinity);
}
// 可是面试官说,这么写就没意思了,因而能够换一个递归实现的
function fn(arr) {
const res = [];
arr.forEach(item => {
if (Array.isArray(item)) {
res.push(...fn(item));
} else {
res.push(item);
}
});
return res;
}
// 面试官说,若是不用递归呢,那就迭代吧
function fn(arr) {
const res = [];
// 因为会破坏原数组,最好浅复制一下,保护原数组
const tmp = [...arr];
while (tmp.length) {
const item = tmp.shift();
if (Array.isArray(item)) {
tmp.unshift(...item);
} else {
res.push(item);
}
}
return res;
}
// 方法不少,可是千万别写这种,面试官说若是我里面什么数据类型都有,下面的写法只会返回字符串
function fn(arr) {
return arr.toString().split(',');
}
复制代码
函数签名的问题,怎么处理
// 面试官问了这个问题,千万别答 [1, 2, 3]
[1, 2, 3].map(parseInt); // [1, NaN, NaN]
// 看看 parseInt 的参数声明,人家是有两个参数的。
parseInt: (s: string, radix?: number): number
复制代码
parseInt
接受了 map
的值做为第一个参数,索引做为第二个参数,而后数组自己这个参数没用到,被忽略了,因此计算过程是
parseInt(1, 0); // 1
parseInt(2, 1); // 基数没有 1 的,返回 NaN
parseInt(3, 2); // 二进制不可能出现 3 的,只可能有 0 和 1 ,因此仍是 NaN
复制代码
想输出预期结果很简单 [1, 2, 3].map(i => parseInt(i))
或者 [1, 2, 3].map(Number)
面试官又说,若是我以上两种都不想写,而且要在 parseInt
上面修改,这时候,面试官的意图就出来了,面试官想要的答案是函数柯里化。写一个返回基数是 10
的方法就好。
function radix10ParseInt(s: string): number {
return parseInt(s, 10);
}
[1, 2, 3].map(radix10ParseInt);
复制代码
实现一个函数柯里化
由于你答出了函数柯里化,面试官就会问你怎么实现相似的效果。若是面试官出的题目,参数很少的状况下,能够不写通用的,毕竟功能实现了,可是通用的更好。
function curry(fn, ...args) {
const len = fn.length;
return (...innerArgs) => {
const mergedArgs = [...args, ...innerArgs];
if (len !== mergedArgs.length) {
return curry(fn, ...mergedArgs);
}
return fn(...mergedArgs);
};
}
const addOrigin = (a, b, c, d, e) => a + b + c + d + e;
const add = curry(addOrigin);
const add2 = curry(addOrigin, 1, 2);
add(1)(2, 3)(4, 5);
add(1)(2)(3)(4)(5);
add2(3, 4)(5);
复制代码
Vue
双向绑定的原理,有什么缺陷,Vue 3.0
中作了什么处理,有什么优势
经过 Object.defineProperty
深度遍历全部每一个须要双向绑定的对象的属性,重写 get
, set
方法,在获取属性被获取的时候添加订阅者,在属性修改的时候通知全部订阅者更新。
写一个简单的实现代码,别说,我还真遇到手写简单的,写起来不算难。
function observe(obj) {
for (let prop in obj) {
define(obj, prop, obj[prop]);
}
}
function define(obj, prop, value) {
if (typeof obj[prop] === 'object') {
observe(obj[prop]);
}
const dep = new Dep();
Object.defineProperty(obj, prop, {
enumerable: true,
configurable: true,
get() {
// 当有获取该属性时,证实依赖于该对象,所以被添加进收集器中
if (Dep.target) {
dep.addSub(Dep.target);
}
return value;
},
// 从新设置值时,触发收集器的通知机制
set(newVal) {
value = newVal;
dep.notify();
}
});
}
// 依赖收集器
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => {
sub.update();
});
}
}
Dep.target = null;
class Watcher {
constructor(obj, key, cb) {
Dep.target = this;
this.cb = cb;
this.key = key;
this.obj = obj;
// 触发 get 方法,把这个观察者添加到依赖列表
this.value = obj[key];
Dep.target = null;
}
update() {
this.value = this.obj[this.key];
this.cb(this.value);
}
}
// key
const data = { key: 1 };
observe(data);
new Watcher(data, 'key', val => {
console.log(val);
});
data.key = '2';
// 复制到 浏览器控制台,或者直接用node能够测试
复制代码
Object.defineProperty
的缺陷,其实, Vue
教程列表渲染那章其实说的很清楚了。
1.Object.defineProperty
没法监控到数组下标的变化,致使经过数组下标添加元素,不能实时响应;
补充 Vue 对 push,pop,splice 等八种方法进行了 hack 处理,这些方法引发的变化是能够检测的,因此其余数组的属性也是检测不到的。
2.Object.defineProperty
只能劫持对象的属性,从而须要对每一个对象,每一个属性进行遍历,若是,属性值是对象,还须要深度遍历。
Vue
3.0 用 Proxy
替代了 Object.defineProperty
Proxy
优点:
1.Proxy
能够劫持整个对象,并返回一个新的对象。而且能够代理动态增长的属性。
2.能监听数组的变化。
3.有 13 种挟持操做。
劣势:
Proxy
是 ES6
新增的属性,而且没法 polyfill
。
List diff
实例问题
好比原来的 List
[a, b, c, d]
怎么变成 [a, c, e, d, f]
这类问题,这里就不提供答案了。
虚拟 DOM
的好处
一个很简单的总结: 用 js
对象模拟 DOM
结构,经过 diff
在 JS
对象处理数据更新,避免频繁操做 DOM
,提升性能。
参考连接
浏览器的渲染原理
渲染过程也是问的比较多的。
HTML
解析器解析 HTML
代码, 生成 DOM
树CSS
解析器解析 CSS
代码,生成 CCSOM
树DOM
树和 CSSOM
树合并,生成渲染树总结: 构建DOM
-> 构建CSSOM
-> 构建渲染树 -> 布局 -> 绘制
构建 DOM
和构建 CSSOM
是并行的。
JavaScript
的加载、解析与执行会阻塞 DOM 的构建。 而且因为 JavaScript
能够操做 CSSOM
,不完整的 CSSOM 是没法使用的 ,这时候就先等 CSSOM
构建完成才会执行 JS
文件,所以在遇到有 JS
加载的时候 CSSOM
也会阻塞 DOM
构建。
面向面试的话,答出这些就行了。详细请点击这里: 参考连接
DOMContentLoaded
和 Load
事件的差异
可能面试官问你的问题不是这个,多是 window.load
和 $(document).ready
的差异,其实问题差很少,固然,这个须要答的更多,核心仍是同样的。 DOMContentLoaded
在 DOM
构建完成就触发。
load
在页面全部资源加载完成后才触发,包括静态资源。
Http
缓存策略
强缓存(Expires
和 Cache-Control
)和协商缓存(Last-Modified
和 Etag
)
强缓存表示在缓存期间不须要请求,state code
为 200
Expires
// 表示文件 `Fri, 09 May 2019 09:00:00 GMT` 过时
Expires: Fri, 09 May 2019 09:00:00 GMT
复制代码
Cache-control
// 表示文件 31536000 秒后过时,也就是一年
Cache-control: max-age=31536000
复制代码
若是缓存过时了,咱们就可使用协商缓存来解决问题。协商缓存须要请求,若是缓存有效会返回 304
。
Last-Modified
(res header) 和 If-Modified-Since
(req header)
Last-Modified 表示本地文件最后修改日期,If-Modified-Since
会将 Last-Modified
的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来。缺点是,精度只能到秒,秒内的修改无法检测到,若是文件修改,内容没变,缓存仍是会刷新。因此就有了 Etag
。
ETag
(res header) 和 If-None-Match
(req header)
ETag
相似于文件指纹,If-None-Match
会将当前 ETag
发送给服务器,询问该资源 ETag
是否变更,有变更的话就将新的资源发送回来。而且 ETag
优先级比 Last-Modified
高。
观察者和发布订阅模式的差异
这个在电话面试里面被问到,能够参考 这个连接。
能够想想,Vue
双向绑定用的是观察者,仍是发布订阅。Vuex
呢。
Cors
如何实现同一个主域下所有子域均可以跨域
这个被问到,这里就不提供答案了,
如何实现轮询
不停的发请求和 WebSocket
等。
今年我遇到的面试题大概就这些,有一些零零碎碎的不写了,一些比较热门的好比对象,继承这些没遇到过,反而一些挺冷门的问题遇到了,面试,基础仍是很重要,现场编程能力也很重要的,面试过程当中和面试官聊天仍是很不错的,沟通仍是很重要,必须锻炼本身的沟通能力。第一次写面试总结,写的很差请你们见谅,有错误请指出。里面有些题目的答案没给出,有些是故意的,有些是我自己总结的也很差,但愿在评论区有人给出答案。