面试 - 面试题汇总
列举了一些在面试时遇到的有价值的题目
JS部分
原型题
打印出的值分别是什么?
let A = function(){};
A.prototype.n = 1;
let b = new A();
A.prototype = {
n: 2,
m: 3
};
var c = new A();
console.log(b.n);
console.log(b.m);
console.log(c.n);
console.log(c.m);
解释:因为调用构造函数时会为实例添加一个指向最初原型(prototype)的指针,而非指向构造函数。当构造函数的原型被重写时,原来的原型依旧存在并没有被销毁,所以 b 的原型指针依然指向原来的原型,而重写之后创建的实例的原型指针会指向新的原型。
答案:1;undefined;2;3
延展
将上述代码做一些改动:
let A = function(){};
A.prototype.n = 1;
let b = new A();
A.prototype.n = 2;
var c = new A();
console.log(b.n);
console.log(c.n);
解释:在本例中,并没有重新赋予 A 新的原型,而是修改了属性 n,内存的指针并没有做变动,指向的是同一个原型(prototype)对象,所以当原型对象内的属性被修改时,不论是修改之前创建的还是修改之后创建的对象,其属性都会做出相应的变动。
答案:2;2
延展
let F = function() {};
Object.prototype.a = function() {
console.log('a');
};
Function.prototype.b = function() {
console.log('b');
}
let f = new F();
f.a();
f.b();
F.a();
F.b();
解释:构造函数 F 继承了 Function,所以能调用 Function 上面的 b 方法,又因为 Function 继承了 Object 所以又能调用 Object 上的 a 方法,所以 F.a() 和 F.b() 分别返回 a 和 b。但当使用 new 方法创建实例时,实例 f 的 __proto__ 指向 F 的原型,而不是 F 本身,又因为原型是一个对象,所以原型继承的是 Object 而非 Function,所以实例 f 只能调用 Object上的 a 方法。不能调用 Function 上的 b 方法。
答案:a;f.b is not a function;a;b
说出下列代码的打印结果
Object.prototype.__proto__;
Function.prototype.__proto__;
Object.__proto__;
Object instanceof Function;
Function instanceof Object;
Function.prototype === Function.__proto__;
答案:
Object.prototype.__proto__; //null
Function.prototype.__proto__; //Object.prototype
Object.__proto__; //Function.prototype
Object instanceof Function; //true
Function instanceof Object; //true
Function.prototype === Function.__proto__; //true
变量提升
打印出的值分别是什么?
console.log( a );
var a = 1;
解释:var 声明会被提前到它作用域的最前面,但是他分配的值是没有提前的。
答案:undefined
延展
a();
var a = function(){
console.log('hi')
};
解释:匿名函数的情况与var
一致
答案:TypeError: a is not a function
a();
function a(){
console.log('hi')
};
解释:函数声明提升了函数名和函数体
答案:hi
foo();
var foo = function() {
console.log('foo1');
}
foo();
function foo() {
console.log('foo2');
}
foo();
解释:函数提升优先级高于变量提升,且变量会互相覆盖
答案:foo2、foo1、foo1
隐式转换
“1”+2+”3”+4 = ?
解释:当其他类型的值与字符串相加时(不论字符串在前还是在后),另一项都会被转义成字符串,相加的结果也必定是字符串,例如:”5” + null = 5null,”5” + undefined = 5undefined。
答案:1234
拓展
乘法隐性转换原则:
1、相乘的两个数会先转换成数字类型,只要有一个数是NaN,那么结果就是NaN。
- 5 * “5” = 25
- 5 * null = 0
- 5 * “a” = NaN
- 5 * undefined = NaN
2、如果Infinity与0相乘,结果是NaN。
除法隐性转换原则:
与乘法一致,增加了:0 / 0 = NaN
- 5 / “5” = 1
- 5 / “a” = NaN
- 5 / undefined = NaN
- 5 / null = Infinity
- 5 / 0 = Infinity
- 0 / 0 = NaN
减法隐性转换原则:
与乘法一致
- 5 - “a” = NaN
- 5 - undefined = NaN
- 5 - null = 5
- 5 - “” = 5
Null 的转换原则:
null 在一元计算中(除了与字符串相加)都会被转换成数字0。
数组操作
打印出的值分别是什么?
const clothes = ['jacket', 't-shirt'];
clothes.length = 0;
console.log(clothes[0]);
解释:如果赋值 length 小于数组的实际 length,则会将数组进行截断,上题 length 设置为 0 时,clothes 的值为 []。
答案:undefined
延展
const clothes = ['jacket', 't-shirt'];
clothes.length = 5;
console.log(clothes[4]);
解释:当 length 大于数组的实际 length 值时,会使用 undefined 进行填充。
答案:undefined
for…in/of
for…in 和 for…of 有什么区别?
答案:
- in 遍历可枚举属性(包括它的原型链上的可枚举属性),对象、数组皆可使用,循环出来的是 key。
- of 只能遍历可迭代对象,例如 Array,Map,Set,String,TypedArray,arguments,不可迭代对象不能遍历,循环出的是 value。
变量声明
定义变量时,有 var 和无 var 的区别
使用 var 去声明全局变量时,只有在最外层进行声明时,才会被挂载到全局对象上;无 var 去声明变量时,不论这个变量在什么位置,都会被挂载到全局对象上。举个例子:
var b = 1; function foo(){ var a = 10 }; console.log(window.b) // 1 console.log(window.a) // undefined
function foo(){ a = 10 }; console.log(window.a) // 10
用 var 定义的全局变量在挂载到全局对象上后,无法被删除,而无 var 定义的全局变量可以被删除:
var a = 10; delete a; // false b = 10; delete b; // true
let 和 var 有什么区别?
var 存在着变量提升,而 let 没有,看一道经典的变量提升和函数声明提升的面试题:
var a = 99; // 全局变量a f(); // f是函数,虽然定义在调用的后面,但是函数声明会提升到作用域的顶部。 console.log(a); // a=>99,静态作用域,不受执行顺序的影响 function f() { console.log(a); // 当前的a变量是下面变量a声明提升后,默认值undefined var a = 10; console.log(a); // a => 10 }
console.log(aicoder); // 错误:Uncaught ReferenceError ... let aicoder = 'aicoder.com';
var 没有块级作用域,而 let 有;
var 可以重复声明,而 let 不能;
var 定义的全局变量会写入全局对象,而 let 不会;
Js 的内存机制
在创建字符串/对象的时候系统会自动分配内存,当它们不再被使用的时候进行释放(垃圾回收机制)。
如何判定对象不再被使用?
现代浏览器普遍使用的是标记清除算法,将“对象是否可获得”作为判定对象是否被使用的标准。
那么什么是对象是否可获得呢?
举个例子,现在定义一个对象:
let foo = {
name: 'lucy'
}
此时对象{ name: 'lucy' }
是可以通过指针 foo 来获得的,接下来修改 foo 的值:
foo = null
那么此时,指针 foo 的指向变了,指向了对象 null,那么此时对象{ name: 'lucy' }
就没有指针再指向它了,那么它也就被判定为无法被获得,会被执行垃圾回收机制。
由此延伸开来,当两个指针指向同一个对象时:
let foo = {
name: 'lucy'
}
let foo2 = foo;
foo = null;
如果将其中指针 foo 移动到 null,foo2 依旧会保持指向原对象,原对象还是能够被获得,不会被回收。
链式调用
如何实现类似 .slice() 形式的调用
假设现在有一个对象 A,需要给他一个 slice2 方法,那么这就相当于是给对象 A 赋予一个 slice2 的属性,但如果要适配所有的对象,那么应该是在 Object 的原型上定义一个公共的属性,如下:
Object.prototype.slice2 = function(){ console.log('call slice2') };
const A = {};
A.slice2(); // call slice2
逻辑与/或
写出下列代码打印出的值
[] && 1;
null && undefined;
[] || 1;
null || 1;
// output: 1 、null、 []、 1
当逻辑运算的操作项都是布尔值的时候,那返回值也是布尔值,当操作项非布尔值时,返回值也可能是非布尔值。
- 逻辑与(A && B):找到最后一个能被转换成 true 的值
- 逻辑或(A || B):找到第一个能被转换成 true 的值
BOM和DOM有和区别?
BOM(Browser Object Model)是浏览器对象模型,提供与浏览器交互的方法和接口。
DOM(Document Object Model)是文档对象模型,处理网页内容的方法和接口。
onInput和onChange有何区别?
onInput 事件是当输入变化时被触发的事件;
onChange 事件是当输入的内容改变且失去焦点时被触发的事件;
箭头函数
说一下箭头函数和普通函数的区别
- 箭头函数没有自己的执行上下文,所以他的 this 指向的是他的父级;
- 箭头函数不能当做构造函数,对其使用 new 关键字会报错;
- 箭头函数不可以使用 arguments 对象,该对象在函数体内不存在;
- 箭头函数不可使用 yield 命令;
监听对象
如何接听对象的属性变化?
使用Object.defineProperty(obj, props)
方法,设置一个属性的 get 和 set 属性。
举个例子:
var obj={};
Object.defineProperty(obj,'name',{
get:function(){
return data;
},
set:function(newValue){
data=newValue;
console.log('set :',newValue);
//需要触发的渲染函数写在这...
}
});
obj.name="hello"; // 此时触发了set方法,会输出 hello
当然还可以使用 ES6 新增的 Proxy
Proxy 的优势:
Proxy 提供了更多的拦截方法,可以拦截对象的更多操作,如读取属性、设置属性、删除属性、函数调用等。而 Object.defineProperty 只能拦截对象属性的读取和设置操作。
Proxy 可以拦截整个对象,而 Object.defineProperty 只能对单个属性进行拦截。
Proxy 对象提供了可以用于撤销代理的拦截操作。而 Object.defineProperty 一旦对属性进行了定义,就无法撤销。
let obj2 = new Proxy({}, {
set:function(obj, prop, value){
console.log('set :',obj, prop, value);
}
});
obj2.age = 100;
双向绑定
实现一个简易的双向绑定(MVVM)
双向绑定即 UI 能改变数据,数据也能反过来改变 UI,一个典型的例子就是 input 输入框
<input id="input"/>
const data = {};
const input = document.getElementById('input');
// 监听自定义数据的变化
Object.defineProperty(data, 'text', {
set(value) {
input.value = value;
}
});
// 监听用户输入的变化
input.onChange = function(e) {
data.text = e.target.value;
}
空闲回调
说一下 requestIdleCallback 和 requestAnimationFrame
requestIdleCallback:方法回调的执行的前提条件是当前浏览器处于空闲状态;
requestAnimationFrame:每一帧都会调用一次回调方法;
页面的内容都是一帧一帧绘制出来的,目前浏览器大多是 60Hz(60帧/s),每一帧耗时也就是在 16.6ms 左右。那么在这一帧的过程中浏览器又干了些什么呢?
通过上面这张图可以清楚的知道,浏览器一帧会经过下面这几个过程:
接受输入事件
执行事件回调
开始一帧
执行 RAF (RequestAnimationFrame)
页面布局,样式计算
绘制渲染
执行 RIC (RequestIdelCallback)
这一步不是每一帧结束都会执行,只有在一帧的 16.6ms 中做完了前面 6 件事儿且还有剩余时间,才会执行。如果一帧执行结束后还有时间执行 RIC 事件,那么下一帧需要在事件执行结束才能继续渲染,所以 RIC 执行不要超过 30ms,如果长时间不将控制权交还给浏览器,会影响下一帧的渲染,导致页面出现卡顿和事件响应不及时
属性名表达式
说出下列代码打印的值
let a = {a: 10};
let b = {b: 10};
let obj = {
a: 10
};
obj[b] = 20;
console.log(obj[a]);
打印的结果是 20
对于属性名表达式,如果键名是一个对象的话,那么会自动转成[object object]
字符串,所以最后 obj 对象的结构为{a:10,'[object Object]':20}
,而 a 也是对象,所以最后的结果是 20
React部分
手写 useState
下面是 useState 的伪代码:
let memorizedState = [] // 存放 hooks
let cursor = 0 // 在重新渲染的时候需重置为 0
function useState(intialState) {
memorizedState[cursor] = memeorizedState[cursor] || initialState // 获取重新渲染之前的值,如果没有则使用默认值
const currentCursor = cursor; // 闭包会记住当前的cursor
function setState(newState) {
memorizedState[currentCursor] = newState
render()
}
return [ memorizedState[cursor++], setState]
}
这里存在 memorizedState 数组的原因是,一个页面里面可能存在多个 useState,在调用的时候通过 cursor 去获取当前的 state
CSS 部分
消除空隙
两个设置了display:inline-block
的元素中间常常会有一段空隙,如何去消除这段空隙?
方案一:
空隙其实是代码中的空格或者换行符导致的,所以最快捷的方式是清除这些空格,除了手动删除空格外还可以通过编译时的压缩代码来实现。
<span></span>
<span></span>
// 修改为
<span></span><span></span>
方案二:
既然将空格视作字符,那么设置他的父级的字体大小为0,那么空格就不占空间了。
高度自适应
一个高度自适应的 div 内有两个div,一个高度100px,希望另一个填满剩下的高度,有哪些方案?
方案一:使用计算属性
.autoHeight{ height:calc(100% - 100px) };
方案二:使用弹性布局
.container{ display:flex; flex-direction:column };
.autoHeight{ flex:1 };
方案三:使用绝对定位
.container{ position:relative };
.autoHeight{ position:absolute; top:100px; bottom:0 }
浏览器部分
defer & async
解释一下 script 标签中 defer 和 async 的区别
首先来看一下没有加任何属性的 script 标签的加载和运行过程,绿色代表的是解析 HTML,灰色代表 HTML 解析暂停,蓝色代表加载脚本,红色代表运行脚本。
可以看到默认模式下,如果遇到了脚本,会立即暂停后续 HTML 的解析并开始加载脚本,脚本加载完成之后立即运行。
而添加了 async 属性后,脚本的加载不会暂停 HTML 的解析,且和默认模式下一样,加载完成后立即运行脚本,但是他不会按照代码的书写顺序来执行代码。
最后是添加了 defer 属性,他也是异步的,不会阻塞 HTML 的解析,且会在所有 HTML 解析完成之后再运行脚本。
使用原则:
- 如果当前脚本不依赖其他脚本,则使用 async;
- 如果当前脚本依赖其他脚本或者被其他脚本依赖,则使用 defer;
- 如果脚本较小且被其他脚本所依赖,则不适用任何属性;
除此之外还有设置了type="module"
属性的 script 标签,如下图所示:
为什么说DOM操作耗时?
- 线程切换:浏览器为了避免渲染引擎和 JS 引擎同时修改页面而造成渲染结果不一致的情况,要求同一时间只能运行一个引擎,引擎在切换的时候会占用时间;
- 重新渲染:如果在操作 DOM 时涉及到元素、样式的修改,会引起浏览器的重排和重绘;
如何判断页面已经加载完毕?
window.onload
事件触发代表页面中的DOM
、CSS
、JS
、图片已经全部加载完毕。window.onload = function() {};
DOMContentLoaded
事件触发代表初始的HTML
被完全加载和解析,不需要等待CSS
,JS
,图片加载document.addEventListener("DOMContentLoaded", ready);
GET&POST请求有什么区别?
- GET 请求在浏览器回退和刷新时是无害的,而 POST 请求会告知用户数据会被重新提交;
- GET 请求可以被缓存,POST 请求不可以被缓存,除非在响应头中包含合适的 Cache-Control/Expires 字段;
- GET 请求一般不具有请求体,因此只能进行 url 编码且有长度限制,而 POST 请求支持多种编码方式且无长度限制。
- GET 请求的安全性较差,数据被暴露在浏览器的URL中,POST请求的安全性较好,数据不会暴露在URL中;
面试 - 面试题汇总