面试 - 面试题汇总

列举了一些在面试时遇到的有价值的题目

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 有什么区别?

答案:

  1. in 遍历可枚举属性(包括它的原型链上的可枚举属性),对象、数组皆可使用,循环出来的是 key。
  2. of 只能遍历可迭代对象,例如 Array,Map,Set,String,TypedArray,arguments,不可迭代对象不能遍历,循环出的是 value。

变量声明

定义变量时,有 var 和无 var 的区别

  1. 使用 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
    
  2. 用 var 定义的全局变量在挂载到全局对象上后,无法被删除,而无 var 定义的全局变量可以被删除:

    var a = 10;
    delete a; // false
    b = 10;
    delete b; // true
    

let 和 var 有什么区别?

  1. 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';
    
  2. var 没有块级作用域,而 let 有;

  3. var 可以重复声明,而 let 不能;

  4. 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 事件是当输入的内容改变且失去焦点时被触发的事件;

箭头函数

说一下箭头函数和普通函数的区别

  1. 箭头函数没有自己的执行上下文,所以他的 this 指向的是他的父级;
  2. 箭头函数不能当做构造函数,对其使用 new 关键字会报错;
  3. 箭头函数不可以使用 arguments 对象,该对象在函数体内不存在;
  4. 箭头函数不可使用 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 的优势:

  1. Proxy 提供了更多的拦截方法,可以拦截对象的更多操作,如读取属性、设置属性、删除属性、函数调用等。而 Object.defineProperty 只能拦截对象属性的读取和设置操作。

  2. Proxy 可以拦截整个对象,而 Object.defineProperty 只能对单个属性进行拦截。

  3. 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 左右。那么在这一帧的过程中浏览器又干了些什么呢?

通过上面这张图可以清楚的知道,浏览器一帧会经过下面这几个过程:

  1. 接受输入事件

  2. 执行事件回调

  3. 开始一帧

  4. 执行 RAF (RequestAnimationFrame)

  5. 页面布局,样式计算

  6. 绘制渲染

  7. 执行 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 的解析并开始加载脚本,脚本加载完成之后立即运行。

defult

而添加了 async 属性后,脚本的加载不会暂停 HTML 的解析,且和默认模式下一样,加载完成后立即运行脚本,但是他不会按照代码的书写顺序来执行代码。

async

最后是添加了 defer 属性,他也是异步的,不会阻塞 HTML 的解析,且会在所有 HTML 解析完成之后再运行脚本。

defer

使用原则:

  1. 如果当前脚本不依赖其他脚本,则使用 async;
  2. 如果当前脚本依赖其他脚本或者被其他脚本依赖,则使用 defer;
  3. 如果脚本较小且被其他脚本所依赖,则不适用任何属性;

除此之外还有设置了type="module"属性的 script 标签,如下图所示:

为什么说DOM操作耗时?

  1. 线程切换:浏览器为了避免渲染引擎和 JS 引擎同时修改页面而造成渲染结果不一致的情况,要求同一时间只能运行一个引擎,引擎在切换的时候会占用时间;
  2. 重新渲染:如果在操作 DOM 时涉及到元素、样式的修改,会引起浏览器的重排和重绘;

如何判断页面已经加载完毕?

  • window.onload 事件触发代表页面中的 DOMCSSJS、图片已经全部加载完毕。

    window.onload = function() {};
    
  • DOMContentLoaded 事件触发代表初始的 HTML 被完全加载和解析,不需要等待 CSSJS,图片加载

    document.addEventListener("DOMContentLoaded", ready);
    

GET&POST请求有什么区别?

  1. GET 请求在浏览器回退和刷新时是无害的,而 POST 请求会告知用户数据会被重新提交;
  2. GET 请求可以被缓存,POST 请求不可以被缓存,除非在响应头中包含合适的 Cache-Control/Expires 字段;
  3. GET 请求一般不具有请求体,因此只能进行 url 编码且有长度限制,而 POST 请求支持多种编码方式且无长度限制。
  4. GET 请求的安全性较差,数据被暴露在浏览器的URL中,POST请求的安全性较好,数据不会暴露在URL中;
作者

BiteByte

发布于

2021-04-19

更新于

2024-11-15

许可协议