关于前端面试那些题
JavaScript 部分
this 以及 this的指向
this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。
-
第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。
-
匿名函数自调和回调函数里的 this 指向 window。 严格模式(usestrict)下,this->undefined因为这类函数调用时,前边即没有.,也没有new!
"use strict"; (function () { console.log(this) // 'use strict' -> undefined 非'use strict' this -> window })(); // 匿名函数自调 var arr = [1] arr.forEach(function () { // 回调函数 console.log(this) // 'use strict' -> undefined 非'use strict' this -> window });
-
箭头函数里的this,指向当前函数之外最近的作用域中的this。
-
-
第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。
const obj = { name: 'lumi', fn: function() { console.log(this.name) } } obj.fn() // this 指向 obj
-
第三种是构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。
function obj(name) { this.name = name; this.fn = function() { console.log(this.name) } } const person = new obj('lumi') person.fn(); // lumi // fn的this 指向 person对象
-
第四种是 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。
这四种方式,使用构造器调用模式的优先级最高,然后是 apply、call 和 bind 调用模式,然后是方法调用模式,然后是函数调用模式。
call、bind、apply
作用:改变函数执行时的指向,改变this的指向
// 例子:
function fn(..args) {
console.log(this, 'this')
console.log(args, 'args')
}
const obj = {
name: 'lumi'
}
call()、apply()
aplly()接收两个参数,第一个参数为 this 的指向,第二个参数为参数列表。
call() 第一个参数为this指向,后面传入的是一个参数列表。
只是暂时改变this指向。当第一个参数为null
、undefined
的时候,默认指向window。
fn.call(obj,1,2)
fn.apply(obj, [1,2])
// 结果
// this 指向 obj { name: 'lumi'}
// args: [1,2]
fn(1,2)
// 结果
// this 指向 window
// args:[1,2]
bind()
bind方法和call很相似,第一参数也是this
的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入)
改变this
指向后不会立即执行,而是返回一个永久改变this
指向的函数(生成一个新的函数)
const bindFn = fn.bind(obj)
bindFn(1,2)
// 结果:this 指向 obj
ES6 常见
箭头函数
特点:
- 不能作为构造函数,无法使用 new
- 函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象
- 不能使用 argument 对象
- 单命令行时可以不写 return; 返回对象时,需要用括号包裹。
var sum = (num1, num2) => { return num1 + num2; }
// 等同于
var sum = (num1, num2) => num1 + num2;
let getTempItem = id => ({ id: id, name: "Temp" });
// 等同于
let getTempItem = id => { return {id: id, name: "Temp"} };
数据类型
- 基本类型(栈存储):String、Number、Boolean、Undefind、Null、Symbol
- 引用类型(堆存储):Object、Array、Function
数据类型判断
typeof
:其中数组、对象、null都会被判断为object,其他判断都正确。instanceof
:只能正确判断引用类型数据类型constructor
:判断数据类型;对象实例通过其访问它的构造函数Object.prototype.toString.call()
: 使用 Object 对象的原型方法 toString 来判断数据类型
判断数组
Object.prototype.toString.call()
- 通过原型链:
obj.__proto__ === Array.prototype
Array.isArray()
instanceof
Array.prototype.isPrototypeOf
rest 运算符(剩余运算符)
用于解构数组和对象,扩展运算符(...
)被用在函数形参上时,它可以把一个分离的参数序列整合成一个数组
经常用于获取函数的多余参数,或者像上面这样处理函数参数个数不确定的情况。
总结
扩展运算符和rest运算符是逆运算
- 扩展运算符:数组=>分割序列
- rest运算符:分割序列=>数组
Set
- 类似于数组,但成员值唯一。
- 会比较类型, 5 不等于 ’5‘
- null、undefined、NaN不会被过滤
- 本身是一个构造函数,使用new实例化
使用场景:数组/字符串去重
属性方法:add、delete、has、clear、size
遍历:forEach遍历
// 去重
[...new Set([2, 3, 5, 4, 5, 2, 2])] // [2 3 5 4]
[...new Set('ababbc')].join('') // "abc"
// 属性方法
let s = new Set()
s.add(1).add(2).add(2) //{1,2}
s.delete(2) //{1}
s.has(1) //true
s.clear() //{}
s.size //0 注意size是属性,不是方法的调用
Map
类似于对象,也是键值对的集合,但属性不限于字符串,提供了“值—值”的对应,是一种更完善的 Hash 结构实现。只有引用地址一样,map结构才能视为同一个键。
与Object的区别
- Object键只能是字符串/Symbol,但Map键可以是任意值
- Map键值是有序的,对象则不是
- Map 的键值对个数可以从 size 属性获取,而 Object 的键值对个数只能手动计算(通过keys数组个数)。
- Object 都有自己的原型,原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。
属性方法:set、get、delete、has、clear、size、keys、values
const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false
m.size //0
遍历:通过 forEach 和 for…of,获取key与对应的value
//注意:value在前
map.forEach(function (value, key) {
console.log(key, value);
});
for (let o of map) {
console.log(o) //[key,value]
}
Reflect
是一个内置对象,提供了一系列用于操作对象的方法。Reflect 将 Object 对象的一些明显属于语言内部的方法(in、delete),放到Reflect对象上(Reflect.get、Reflect.set)。
WeakMap
EventLoop 事件循环
众所周知,JS是 单线程,有且只有一个调用栈,先执行同步任务,再执行异步任务。
宏任务
- 回调函数
- script内容
- setTimeout 和 setInterval、setImmediate
- requestAnimationFrame
- i/o操作
- ui rendering 渲染
微任务
一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。
- Promise.then
- MutaionObserver
- Object.observe(已废弃;Proxy 对象替代)
- process.nextTick(Node.js)
async 与 await
async
是异步的意思,await
则可以理解为 async wait
。所以可以理解async
就是用来声明一个异步方法,而 await
是用来等待异步方法执行。
async
:用来声明一个异步方法,返回一个promise 对象。await
:用来等待异步方法执行。await
命令后面是一个Promise
对象,返回该对象的结果。如果不是Promise
对象,就直接返回对应的值。不管await
后面跟着的是什么,await
都会阻塞后面的代码
执行机制
-
执行一个宏任务,如果遇到微任务就将它放到微任务的事件队列中
-
当前宏任务执行完成后,会查看微任务的事件队列,然后将里面的所有微任务依次执行完
举例说明
例子🌰1
console.log('script start' ) // 宏任务 task1
setTimeout(()=>{
console.log('setTimeout')
}, 0) // 宏任务 task2
new Promise((resolve, reject)=>{
console.log('new Promise') // 宏任务task1
resolve()
}).then(()=>{
console.log('promise then') // 微任务 micortask1
})
console.log('script end') // 宏任务task1
分析:
- 遇到console ,直接打印 script start
- 遇到定时器setTimeout,属于新的宏任务,留着
- 遇到promise,直接打印 new Promise
- 接着是promise.then,属于微任务,留着
- 遇到 console,直接打印 script end
- 第一轮宏任务(即主线程)执行结束,查看微任务列表,发现promise.then 回调,执行打印 promise end
- 当所有微任务执行结束后,执行下一个宏任务setTimeout,打印 setTimeout
例子🌰2
async function async1() {
console.log('async1 start') //task1.2.1
await async2() // task1.2.2
console.log('async1 end') // microtask1
}
async function async2() {
console.log('async2')
}
console.log('script start') // task1.1
setTimeout(function () {
console.log('settimeout')
}) // task2
async1() //task1.2
new Promise(function (resolve) {
console.log('promise1') // task1.3
resolve() // task 1.4
}).then(function () {
console.log('promise2') // microtask2
})
console.log('script end') // task1.5
分析:
- 遇到 console,执行打印 script start
- 遇到定时器,属于新的宏任务,留着
- 遇到
async1
,执行,打印async1 start
,遇到async2
, 执行打印async2
, await会阻塞后面的代码(即加入微任务列表),跳出去执行同步任务 - 遇到 promise,执行打印 promise1
- 遇到promise.then,属于微任务,留着
- 遇到console,执行打印 script end
- 第一轮宏任务执行结束,检查微任务列表,先执行await后的代码,即打印
async1 end
, 接着执行then,打印promise 2
- 微任务执行结束,执行下一个宏任务定时器,打印 setTimeout
类似Map,但键名只能是对象,键名所指向的对象,不计入垃圾回收机制。
EventFlow 事件流
存在三个阶段:事件捕获、目标阶段、事件冒泡。Dom
标准事件流的触发的先后顺序为:先捕获再冒泡。即当触发dom
事件时,会先进行事件捕获,捕获到事件源之后通过事件传播进行事件冒泡。
事件冒泡
当一个元素接收到事件的时候,会把他接收到的事件传给自己的父级,一直到 window/dom
。(该事件仅指click、dbclick等事件,而非绑定的函数方法)
例如:A元素包含B元素,点击B元素的同时就会冒泡触发A元素的点击事件。
事件捕获
当鼠标点击或者触发dom
事件时(被触发dom
事件的这个元素被叫作事件源),浏览器会从根节点 =>事件源(由外到内)进行事件传播。
与冒泡不同是,事件的传播方向,捕获是由外到内的,冒泡是由内到外的。
事件委托
又称事件代理。即利用事件冒泡,将子元素事件绑定到父元素上,如果子元素阻止了冒泡,则委托无法实现。
优点:
- 替代循环绑定事件的操作,减少内存消耗,提高性能。
- 简化dom节点更新时相应事件的更新。
缺点:
- 事件委托基于冒泡,对于不冒泡的事件不支持。
- 层级过多,冒泡过程中,可能会被某层阻止掉。
- 理论上委托会导致浏览器频繁调用处理函数,虽然很可能不需要处理。所以建议就近委托,比如在
table
上代理td
,而不是在document
上代理td
。
原型
了解原型需要先了解构造函数。由于Js 没有类的概念(es6有class关键词,使用方法后续拓展),所以使用 构造函数来实现继承机制。
构造函数
Js通过构造函数生成实例。但产生了一个问题:无法共享公共属性,构造函数中通过 this赋值的属性方法是每个实例独有的。
所以原型对象就是用来存储共享属性和方法的。
// 🌰使用构造函数创造一个实例
// 构造函数
function Student(name, age) {
this.name = name
this.age = age
}
// 实例
const student1 = new Student('lumi', 18)
原型对象
每个函数在生成的时候,都会创建一个属性 prototype
, 这个属性指向一个对象,即 原型对象。
原型对象中有一个属性 constructor
, 指向该函数,这样两者就联系起来了。
原型链
原型链就是实例对象和原型对象之间的联系。每个构造函数创建的每一个实例,都有一个属性 __proto__
,这个属性指向构造函数的原型对象。通过该属性可以一步一步向上查找形成一个链式结构,称为 原型链。
如果通过实例对象的
__proto__
属性赋值 ,会改变其构造函数的原型对象,从而被所有该构造函数创建的实例所共享。
// 🌰:
function Student(name, age) {
this.name = name;
this.age = age;
}
// 往原型对象添加共享的方法属性
Student.prototype.school = '北京大学'
Student.prototype.goToClass = function() { console.log(`${this.name}上课了`) }
// 创建实例对象
const s1 = new Student('lumi', 13);
const s2 = new Student('lucy', 15);
s1.goToClass(); // lumi上课了
s2.goToClass(); // lucy上课了
s2.hasOwnProperty('goToClass') // false,说明不是实例独有的,是共享的
s1.__proto__.leave = function() {
console.log(`${this.name}放学了`)
} // 往原型对象里添加方法
s2.leave() // lucy放学了 (说明s1添加了方法,s2能共享这个方法)
注意⚠️
生产环境中,不建议使用
__proto__
,避免环境产生依赖。可以使用Object.getPrototypeOf
方法来获取实例对象原型,然后再为原型添加方法和属性。
补充:原型链的尽头是null
,详情见:
Proxy
Proxy代理:在目标对象之前假设一层拦截,可以对外界的访问进行改写。ES6提供原生的Proxy构造函数。
var proxy = new Proxy(target, handler)
// target: 拦截的对象
// handler: 定制拦截行为,可以拦截行为有几十种,仅介绍常见的几种。
常见拦截行为
- get捕获器:用于拦截对象属性读取
- set捕获器:用于拦截对象属性赋值
- has捕获器:用于拦截判断target对象中是否含有某个属性
- deleteProperty捕获器:用于拦截删除target对象属性的操作
// 🌰:
let person = {
name: 'lumi'
}
var proxy = new Proxy(person, {
get: function(target, key) {
if (key in target) {
return target[key];
}else{
return 'not found'
}
},
set: function(target, key, value) {
target[key] = value
return true
},
has: (target, key) => {
return key in target
},
deleteProperty: (target, key) => {
if (key === 'name') {
throw new Error('name不可被删除')
} else {
return delete target[key]
}
}
})
console.log(proxy.name) // lumi
console.log(proxy.age) // not found
proxy.age = 18
console.log(proxy.age) // 18
console.log('age' in proxy) // true
console.log('sex' in proxy) // false
// delete proxy.name // name 不可被删除
delete proxy.age
console.log('age' in proxy) // false
模块化
将js分割成不同职责的js,⽤于解决全局变量污染、变量冲突、代码冗余等问题,提高代码可维护性、可拓展性、复用性。
自执行函数实现模块化
通过函数作用域解决了命名冲突、污染全局作用域的问题。
// 自执行函数实现模块化
(function () {
var a = 1;
console.log(a); // 1
})();
(function () {
var a = 2;
console.log(a); // 2
})();
AMD
异步模块定义,采用异步方法加载模块,模块加载不影响后面语句运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到所有依赖项都加载完成之后,这个回调函数才会运行。【前置依赖】
CMD
公共模块定义,可以使用 require 同步加载依赖,也可以使用 require.async 来异步加载依赖。【后置依赖】
CommonJs
Node的⼀种模块同步加载规范,⼀个⽂件即是⼀个模块。用在Node端(服务端),加载速度很快,所以可以使用同步方法。
- 使⽤时require引⼊。
- module.exports 是 CommonJS 的⼀个 exports 变量,提供对外的接⼝。
- 输出为一个值的拷贝。
ESModule
是ES6提供的官方js模块化方案。目前浏览器还不能全面支持 ESModule
的语法,需要用 babel 进行解析。
通过export命令显式指定输出的代码。属于编译时加载,⽐Commonjs效率⾼。可以按需加载指定⽅法。效率⾼。
- export defalut 与 export 是 ES6 Module 中对外暴露数据的。 export defalut 是向外部默认暴露数据,使⽤时 import 引⼊时需要为该模块指定⼀个任意名称,import 后不可以使⽤{};
- export 是单个暴露数据或⽅法,利⽤ import{}来引⼊,并且{}内名称与 export ⼀⼀对应,{}也可以使⽤ as 为某个数据或⽅法指定新的名称;
- 输出为一个值的引用。
转载自:https://juejin.cn/post/7352845644737183771