35道面向初中级前端的基础面试题

72 阅读13分钟

参考资料:

2. 跨域是什么?

  • 原因

浏览器的同源策略导致了跨域

  • 作用

用于隔离潜在恶意文件的重要安全机制

  • 解决
  1. jsonp ,允许 script 加载第三方资源

  2. 反向代理(nginx 服务内部配置 Access-Control-Allow-Origin *)

  3. cors 前后端协作设置请求头部,Access-Control-Allow-Origin 等头部信息

  4. iframe 嵌套通讯,postmessage

参考资料:

3. JSONP 是什么?

这是我认为写得比较通俗易懂的一篇文章jsonp原理详解——终于搞清楚jsonp是啥了

4. 事件绑定的方式

  • 嵌入dom

按钮

  • 直接绑定

btn.onclick = function(){}

  • 事件监听

btn.addEventListener('click',function(){})

5. 事件委托

事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。所有用到按钮的事件(多数鼠标事件和键盘事件)都适合采用事件委托技术, 使用事件委托可以节省内存。

  • 苹果
  • 香蕉
  • 凤梨

// good

document.querySelector('ul').onclick = (event) => {

let target = event.target

if (target.nodeName === 'LI') {

console.log(target.innerHTML)

}

}

// bad

document.querySelectorAll('li').forEach((e) => {

e.onclick = function() {

console.log(this.innerHTML)

}

})

6. 事件循环

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

7. 如何自定义事件

新模式

const div = document.createElement('div') // 不创建元素,直接用 window 对象也可以

const event = new Event('build')

div.addEventListener('build', function(e) {

console.log(111)

})

div.dispatchEvent(event)

过时的模式

  1. 原生提供了3个方法实现自定义事件

  2. document.createEvent('Event') 创建事件

  3. initEvent 初始化事件

  4. dispatchEvent 触发事件

const events = {}

function registerEvent(name) {

const event = document.createEvent('Event')

event.initEvent(name, true, true) // 事件名称,是否允许冒泡,该事件的默认动作是否可以被取消

events[name] = event

}

function triggerEvent(name) {

window.dispatchEvent(events[name])

}

registerEvent('resize') // 注册 resize 事件

triggerEvent('resize') // 触发 resize 事件

MDN

8. target 和 currentTarget 区别

  • event.target 返回触发事件的元素

  • event.currentTarget 返回绑定事件的元素

9. prototype 和 proto 的关系是什么

  1. prototype 用于访问函数的原型对象。

  2. __proto__ 用于访问对象实例的原型对象(或者使用 Object.getPrototypeOf())。

function Test() {}

const test = new Test()

test.proto == Test.prototype // true

也就是说,函数拥有 prototype 属性,对象实例拥有 __proto__ 属性,它们都是用来访问原型对象的。

函数有点特别,它不仅是个函数,还是个对象。所以它也有 __proto__ 属性。

为什么会这样呢?因为函数是内置构造函数 Function 的实例:

const test = new Function('function Test(){}')

test.proto == Function.prototype // true

所以函数能通过 __proto__ 访问它的原型对象。

由于 prototype 是一个对象,所以它也可以通过 __proto__ 访问它的原型对象。对象的原型对象,那自然是 Object.prototype 了。

function Test() {}

Test.prototype.proto == Object.prototype // true

这样看起来好像有点复杂,我们可以换个角度来看。Object 其实也是内置构造函数:

const obj = new Object()

obj.proto == Object.prototype // true

从这一点来看,是不是更好理解一点。

为了防止无休止的循环下去,所以 Object.prototype.__proto__ 是指向 null 的,null 是万物的终点。

Object.prototype.proto == null // true

既然 null 是万物的终点,那使用 Object.create(null) 创建的对象是没有 __proto__ 属性的,也没有 prototype 属性。

10. 原型继承

所有的 JS 对象(JS 函数是 prototype)都有一个 __proto__ 属性,指向它的原型对象。当试图访问一个对象的属性时,如果没有在该对象上找到,它还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。

11. 继承

寄生组合式继承

function SuperType(name) {

this.name = name

this.colors = ['red']

}

SuperType.prototype.sayName = function() {

console.log(this.name)

}

// 继承实例属性

function SubType(name, age) {

SuperType.call(this, name)

this.age = age

}

function inheritPrototype(subType, superType) {

let prototype = Object.create(superType.prototype)

prototype.constructor = subType

subType.prototype = prototype

}

// 继承原型方法

inheritPrototype(SubType, SuperType)

// 定义自己的原型方法

SubType.prototype.sayAge = function() {

console.log(this.age)

}

12. 闭包

闭包是指有权访问另一个函数作用域中的变量的函数。

function sayHi(name) {

return () => {

console.log(Hi! ${name})

}

}

const test = sayHi('xiaoming')

test() // Hi! xiaoming

虽然sayHi函数已经执行完毕,但是其活动对象也不会被销毁,因为test函数仍然引用着sayHi函数中的变量name,这就是闭包。

但也因为闭包引用着另一个函数的变量,导致另一个函数即使不使用了也无法销毁,所以闭包使用过多,会占用较多的内存,这也是一个副作用。

利用闭包实现私有属性

const test = (function () {

let value = 0

return {

getVal() { return value },

setVal(val) { value = val }

}

})()

上面的代码实现了一个私有属性 value,它只能用过 getVal() 来取值,通过 setVal(val) 来设置值。

13. 内存回收

在 JS 中,有两种内存回收算法。第一种是引用计数垃圾收集,第二种是标记-清除算法(从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法)。

引用计数垃圾收集

如果一个对象没有被其他对象引用,那它将被垃圾回收机制回收。

let o = { a: 1 }

一个对象被创建,并被 o 引用。

o = null

刚才被 o 引用的对象现在是零引用,将会被回收。

循环引用

引用计数垃圾收集有一个缺点,就是循环引用会造成对象无法被回收。

function f(){

var o = {};

var o2 = {};

o.a = o2; // o 引用 o2

o2.a = o; // o2 引用 o

return "azerty";

}

f();

在 f() 执行后,函数的局部变量已经没用了,一般来说,这些局部变量都会被回收。但上述例子中,o 和 o2 形成了循环引用,导致无法被回收。

标记-清除算法

这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。

对于刚才的例子来说,在 f() 执行后,由于 o 和 o2 从全局对象出发无法获取到,所以它们将会被回收。

高效使用内存

在 JS 中能形成作用域的有函数、全局作用域、with,在 es6 还有块作用域。局部变量随着函数作用域销毁而被释放,全局作用域需要进程退出才能释放或者使用 delete 和赋空值 null undefined

在 V8 中用 delete 删除对象可能会干扰 V8 的优化,所以最好通过赋值方式解除引用。

参考资料:

14. 有一个函数,参数是一个函数,返回值也是一个函数,返回的函数功能和入参的函数相似,但这个函数只能执行3次,再次执行无效,如何实现

这个题目是考察闭包的使用

function sayHi() {

console.log('hi')

}

function threeTimes(fn) {

let times = 0

return () => {

if (times++ < 3) {

fn()

}

}

}

const newFn = threeTimes(sayHi)

newFn()

newFn()

newFn()

newFn()

newFn() // 后面两次执行都无任何反应

通过闭包变量 times 来控制函数的执行

15. 实现add函数,让add(a)(b)和add(a,b)两种调用结果相同

实现1

function add(a, b) {

if (b === undefined) {

return function(x) {

return a + x

}

}

return a + b

}

实现2——柯里化

function curry(fn, ...args1) {

// length 是函数对象的一个属性值,指该函数有多少个必须要传入的参数,即形参的个数。

if (fn.length == args1.length) {

return fn(...args1)

}

return function(...args2) {

return curry(fn, ...args1, ...args2)

}

}

function add(a, b) {

return a + b

}

console.log(curry(add, 1)(2)) // 3

console.log(curry(add, 1, 2)) // 3

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

优点

  • 交互性更好。来自服务器的新内容可以动态更改,无需重新加载整个页面。

  • 减少与服务器的连接,因为脚本和样式只需要被请求一次。

  • 状态可以维护在一个页面上。JavaScript 变量和 DOM 状态将得到保持,因为主容器页面未被重新加载。

  • 基本上包括大部分 SPA 的优点。

缺点

  • 动态网页很难收藏。

  • 如果 JavaScript 已在浏览器中被禁用,则不起作用。

  • 有些网络爬虫不执行 JavaScript,也不会看到 JavaScript 加载的内容。

  • 基本上包括大部分 SPA 的缺点。

参考资料:

17. Ajax和Fetch区别

  • ajax是使用XMLHttpRequest对象发起的,但是用起来很麻烦,所以ES6新规范就有了fetch,fetch发一个请求不用像ajax那样写一大堆代码。

  • 使用fetch无法取消一个请求,这是因为fetch基于Promise,而Promise无法做到这一点。

  • 在默认情况下,fetch不会接受或者发送cookies

  • fetch没有办法原生监测请求的进度,而XMLHttpRequest可以

  • fetch只对网络请求报错,对400,500都当做成功的请求,需要封装去处理

  • fetch由于是ES6规范,兼容性上比不上XMLHttpRequest

18. 变量提升

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

变量提升(hoisting)是用于解释代码中变量声明行为的术语。使用var关键字声明或初始化的变量,会将声明语句“提升”到当前作用域的顶部。 但是,只有声明才会触发提升,赋值语句(如果有的话)将保持原样。

19. 使用let、var和const创建变量有什么区别

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

var 声明的全局变量和函数都会成为 window 对象的属性和方法。使用 let 和 const 的顶级声明不会定义在全局上下文中,但在作用域链解析上效果是一样的。

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';

20. 对象浅拷贝和深拷贝有什么区别

JS 中,除了基本数据类型,还存在对象、数组这种引用类型。 基本数据类型,拷贝是直接拷贝变量的值,而引用类型拷贝的其实是变量的地址。

let o1 = {a: 1}

let o2 = o1

在这种情况下,如果改变 o1o2 其中一个值的话,另一个也会变,因为它们都指向同一个地址。

o2.a = 3

console.log(o1.a) // 3

而浅拷贝和深拷贝就是在这个基础之上做的区分,如果在拷贝这个对象的时候,只对基本数据类型进行了拷贝,而对引用数据类型只是进行了引用的传递,而没有重新创建一个新的对象,则认为是浅拷贝。反之,在对引用数据类型进行拷贝的时候,创建了一个新的对象,并且复制其内的成员变量,则认为是深拷贝。

21. 怎么实现对象深拷贝

这种方法有缺陷,详情请看关于JSON.parse(JSON.stringify(obj))实现深拷贝应该注意的坑

let o1 = {a:{

b:1

}

}

let o2 = JSON.parse(JSON.stringify(o1))

基础版

function deepCopy(target) {

if (typeof target == 'object') {

const result = Array.isArray(target)? [] : {}

for (const key in target) {

if (typeof target[key] == 'object') {

result[key] = deepCopy(target[key])

} else {

result[key] = target[key]

}

}

return result

} else if (typeof target == 'function') {

return eval('(' + test.toString() + ')')

} else {

return target

}

}

完整版

const mapTag = '[object Map]'

const setTag = '[object Set]'

const arrayTag = '[object Array]'

const objectTag = '[object Object]'

const symbolTag = '[object Symbol]'

function deepCopy(origin, map = new WeakMap()) {

if (!origin || !isObject(origin)) return origin

if (typeof origin == 'function') {

return eval('(' + origin.toString() + ')')

}

const objType = getObjType(origin)

const result = createObj(origin, objType)

// 防止循环引用,不会遍历已经在 map 中的对象,因为在上一层正在遍历

if (map.get(origin)) {

return map.get(origin)

}

map.set(origin, result)

// set

if (objType == setTag) {

for (const value of origin) {

result.add(deepCopy(value, map))

}

return result

}

// map

if (objType == mapTag) {

for (const [key, value] of origin) {

result.set(key, deepCopy(value, map))

}

return result

}

// 对象或数组

if (objType == objectTag || objType == arrayTag) {

for (const key in origin) {

result[key] = deepCopy(origin[key], map)

}

return result

}

return result

}

function getObjType(obj) {

return Object.prototype.toString.call(obj)

}

function createObj(obj, type) {

if (type == objectTag) return {}

if (type == arrayTag) return []

if (type == symbolTag) return Object(Symbol.prototype.valueOf.call(obj))

return new obj.constructor(obj)

}

function isObject(origin) {

return typeof origin == 'object' || typeof origin == 'function'

}

如何写出一个惊艳面试官的深拷贝?

22. 数组去重

ES5

function unique(arry) {

const temp = []

arry.forEach(function(item) {

if (temp.indexOf(item) == -1) {

temp.push(item)

}

})

return temp

}

ES6

function unique(arry) {

return Array.from(new Set(arry))

}

23. 数据类型

  1. Undefined

  2. Null

  3. Boolean

  4. Number

  5. String

  6. Object

  7. symbol(ES6新增)

24. 内置函数(原生函数)

  • String

  • Number

  • Boolean

  • Object

  • Function

  • Array

  • Date

  • RegExp

  • Error

  • Symbol

原始值 “I am a string” 并不是一个对象,它只是一个字面量,并且是一个不可变的值。

如果要在这个字面量上执行一些操作,比如获取长度、访问其中某个字符等,那需要将其转换为 String 对象。

幸好,在必要时语言会自动把字符串字面量转换成一个 String 对象,也就是说你并不需要显式创建一个对象。

25. 如何判断数组与对象

Array.isArray([]) // true

Array.isArray({}) // false

typeof [] // "object"

typeof {} // "object"

Object.prototype == [].proto // false

Object.prototype == {}.proto // true

Array.prototype == [].proto // true

Array.prototype == {}.proto // false

26. 自动分号

有时 JavaScript 会自动为代码行补上缺失的分号,即自动分号插入(Automatic SemicolonInsertion,ASI)。

因为如果缺失了必要的 ; ,代码将无法运行,语言的容错性也会降低。ASI 能让我们忽略那些不必要的 ;

请注意,ASI 只在换行符处起作用,而不会在代码行的中间插入分号。

如果 JavaScript 解析器发现代码行可能因为缺失分号而导致错误,那么它就会自动补上分号。并且,只有在代码行末尾与换行符之间除了空格和注释之外没有别的内容时,它才会这样做。

27. 浮点数精度

www.css88.com/archives/73…

28. cookie、localStorage、sessionStorage区别

| 特性 | cookie | localStorage | sessionStorage |

| --- | --- | --- | --- |

| 由谁初始化 | 客户端或服务器,服务器可以使用Set-Cookie请求头。 | 客户端 | 客户端 |

| 数据的生命周期 | 一般由服务器生成,可设置失效时间,如果在浏览器生成,默认是关闭浏览器之后失效 | 永久保存,可清除 | 仅在当前会话有效,关闭页面后清除 |

| 存放数据大小 | 4KB | 5MB | 5MB |

| 与服务器通信 | 每次都会携带在HTTP头中,如果使用cookie保存过多数据会带来性能问题 | 仅在客户端保存 | 仅在客户端保存 |

| 用途 | 一般由服务器生成,用于标识用户身份 | 用于浏览器缓存数据 | 用于浏览器缓存数据 |

| 访问权限 | 任意窗口 | 任意窗口 | 当前页面窗口 |

29. 自执行函数?用于什么场景?好处?

自执行函数:
  1. 声明一个匿名函数

  2. 马上调用这个匿名函数。