如需获取完整版229页PDF面试题,请直接滑到文末。
1. 什么是同源策略?
同源策略可防止 JavaScript 发起跨域请求。源被定义为协议、主机名和端口号的组合。此策略可防止页面上的恶意脚本通过该页面的文档对象模型,访问另一个网页上的敏感数据。
参考资料:
2. 跨域是什么?
- 原因
浏览器的同源策略导致了跨域
- 作用
用于隔离潜在恶意文件的重要安全机制
- 解决
- jsonp ,允许 script 加载第三方资源
- 反向代理(nginx 服务内部配置 Access-Control-Allow-Origin *)
- cors 前后端协作设置请求头部,Access-Control-Allow-Origin 等头部信息
- 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)
过时的模式
- 原生提供了3个方法实现自定义事件
document.createEvent('Event')
创建事件initEvent
初始化事件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 事件
8. target 和 currentTarget 区别
- event.target 返回触发事件的元素
- event.currentTarget 返回绑定事件的元素
9. prototype 和 proto 的关系是什么
prototype
用于访问函数的原型对象。__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
在这种情况下,如果改变 o1
或 o2
其中一个值的话,另一个也会变,因为它们都指向同一个地址。
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. 数据类型
- Undefined
- Null
- Boolean
- Number
- String
- Object
- 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. 浮点数精度
28. cookie、localStorage、sessionStorage区别
特性 | cookie | localStorage | sessionStorage |
---|---|---|---|
由谁初始化 | 客户端或服务器,服务器可以使用Set-Cookie 请求头。 |
客户端 | 客户端 |
数据的生命周期 | 一般由服务器生成,可设置失效时间,如果在浏览器生成,默认是关闭浏览器之后失效 | 永久保存,可清除 | 仅在当前会话有效,关闭页面后清除 |
存放数据大小 | 4KB | 5MB | 5MB |
与服务器通信 | 每次都会携带在HTTP头中,如果使用cookie保存过多数据会带来性能问题 | 仅在客户端保存 | 仅在客户端保存 |
用途 | 一般由服务器生成,用于标识用户身份 | 用于浏览器缓存数据 | 用于浏览器缓存数据 |
访问权限 | 任意窗口 | 任意窗口 | 当前页面窗口 |
29. 自执行函数?用于什么场景?好处?
自执行函数:
- 声明一个匿名函数
- 马上调用这个匿名函数。
作用:创建一个独立的作用域。
好处
- 防止变量弥散到全局,以免各种js库冲突。
- 隔离作用域避免污染,或者截断作用域链,避免闭包造成引用变量无法释放。
- 利用立即执行特性,返回需要的业务函数或对象,避免每次通过条件判断来处理。
场景
一般用于框架、插件等场景
30\. 多个页面之间如何进行通信
有如下几个方式:
- cookie
- web worker
- localeStorage和sessionStorage
31. css动画和js动画的差异
- 代码复杂度,js 动画代码相对复杂一些
- 动画运行时,对动画的控制程度上,js 能够让动画,暂停,取消,终止,css动画不能添加事件
- 动画性能看,js 动画多了一个js 解析的过程,性能不如 css 动画好
32. new一个对象经历了什么
function Test(){}
const test = new Test()
- 创建一个新对象:
const obj = {}
- 设置新对象的constructor属性为构造函数的名称,设置新对象的__proto__属性指向构造函数的prototype对象
obj.constructor = Test
obj.__proto__ = Test.prototype
复制代码
- 使用新对象调用函数,函数中的this被指向新实例对象
Test.call(obj)
- 将初始化完毕的新对象地址,保存到等号左边的变量中
33\. bind、call、apply的区别
call和apply其实是一样的,区别就在于传参时参数是一个一个传或者是以一个数组的方式来传。
call和apply都是在调用时生效,改变调用者的this指向。
let name = 'Jack'
const obj = {name: 'Tom'}
function sayHi() {console.log('Hi! ' + this.name)}
sayHi() // Hi! Jack
sayHi.call(obj) // Hi! Tom
bind也是改变this指向,不过不是在调用时生效,而是返回一个新函数。
const newFunc = sayHi.bind(obj)
newFunc() // Hi! Tom
34. 实现 bind call apply 函数
bind
Function.prototype.bind = function(context, ...extra) {
const self = this
// 这里不能用箭头函数,防止绑定函数为构造函数
return function(...arg) {
return self.call(context, ...extra.concat(arg))
}
}
call
Function.prototype.call = function(context, ...args) {
if (context === null || context === undefined) {
context = window
} else if (!context || context.toString() != '[object Object]') {
context = {}
}
let key = Math.random()
while (context[key]) {
key = Math.random()
}
context[key] = this
const result = context[key](...args)
delete context[key]
return result
}
apply
Function.prototype.apply = function(context, args) {
if (args !== undefined && !Array.isArray(args)) throw '参数必须为数组'
if (context === null || context === undefined) {
context = window
} else if (!context || context.toString() != '[object Object]') {
context = {}
}
let key = Math.random()
while (context[key]) {
key = Math.random()
}
context[key] = this
let result
if (args === undefined) {
const result = context[key]()
} else {
const result = context[key](...args)
}
delete context[key]
return result
}
35. 请简述JavaScript
中的this
。
JS 中的this
是一个相对复杂的概念,不是简单几句能解释清楚的。粗略地讲,函数的调用方式决定了this
的值。我阅读了网上很多关于this
的文章,Arnav Aggrawal 写的比较清楚。this
取值符合以下规则:
- 在调用函数时使用
new
关键字,函数内的this
是一个全新的对象。 - 如果
apply
、call
或bind
方法用于调用、创建一个函数,函数内的 this 就是作为参数传入这些方法的对象。 - 当函数作为对象里的方法被调用时,函数内的
this
是调用该函数的对象。比如当obj.method()
被调用时,函数内的 this 将绑定到obj
对象。 - 如果调用函数不符合上述规则,那么
this
的值指向全局对象(global object)。浏览器环境下this
的值指向window
对象,但是在严格模式下('use strict'
),this
的值为undefined
。 - 如果符合上述多个规则,则较高的规则(1 号最高,4 号最低)将决定
this
的值。 - 如果该函数是 ES2015 中的箭头函数,将忽略上面的所有规则,
this
被设置为它被创建时的上下文。
我平时一直有整理面试题的习惯,有随时跳出舒适圈的准备,不知不觉整理了229页了,在这里分享给大家,有需要的点击这里免费领取题目+解析PDF
篇幅有限,仅展示部分内容
如果你需要这份完整版的面试题+解析,【点击我】就可以了。
希望大家明年的金三银四面试顺利,拿下自己心仪的offer!