JS常见面试题,看看你都会多少?
1. 如何在ES5环境下实现let
这个问题实质上是在回答let和var有什么区别,对于这个问题,我们可以直接查看babel转换前后的结果,看一下在循环中通过let定义的变量是如何解决变量提升的问题
babel在let定义的变量前加了道下划线,避免在块级作用域外访问到该变量,除了对变量名的转换,我们也可以通过自执行函数来模拟块级作用域
(function(){
for(var i = 0; i < 5; i ++){
console.log(i) // 0 1 2 3 4
}
})();
console.log(i) // Uncaught ReferenceError: i is not defined
不过这个问题并没有结束,我们回到var
和let/const
的区别上:
- var声明的变量会挂到window上,而let和const不会
- var声明的变量存在变量提升,而let和const不会
- let和const声明形成块作用域,只能在块作用域里访问,不能跨块访问,也不能跨函数访问
- 同一作用域下let和const不能声明同名变量,而var可以
- 暂时性死区,let和const声明的变量不能在声明前被使用
babel的转化,其实只实现了第2、3、5点
2. 如何在ES5环境下实现const
实现const的关键在于
Object.defineProperty()
这个API,这个API用于在一个对象上增加或修改属性。通过配置属性描述符,可以精确地控制属性行为。Object.defineProperty()
接收三个参数:Object.defineProperty(obj, prop, desc)
参数 | 说明 |
---|---|
obj | 要在其上定义属性的对象 |
prop | 要定义或修改的属性的名称 |
descriptor | 将被定义或修改的属性描述符 |
属性描述符 | 说明 | 默认值 |
---|---|---|
value | 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined | undefined |
get | 一个给属性提供 getter 的方法,如果没有 getter 则为 undefined | undefined |
set | 一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法 | undefined |
writable | 当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false | false |
enumerable | enumerable定义了对象的属性是否可以在 for...in 循环和 Object.keys() 中被枚举 | false |
Configurable | configurable特性表示对象的属性是否可以被删除,以及除value和writable特性外的其他特性是否可以被修改 | false |
对于const不可修改的特性,我们通过设置writable属性来实现
function _const(key, value) {
const desc = {
value,
writable: false
}
Object.defineProperty(window, key, desc)
}
_const('obj', {a: 1}) //定义obj
obj.b = 2 //可以正常给obj的属性赋值
obj = {} //无法赋值新对象
3. 手写call()
call()` 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数 语法:`function.call(thisArg, arg1, arg2, ...)
call()的原理比较简单,由于函数的this指向它的直接调用者,我们变更调用者即完成this指向的变更:
//变更函数调用者示例
function foo() {
console.log(this.name)
}
// 测试
const obj = {
name: '前端脑洞'
}
obj.foo = foo // 变更foo的调用者
obj.foo() // '前端脑洞'
基于以上原理, 我们两句代码就能实现call()
Function.prototype.myCall = function(thisArg, ...args) {
thisArg.fn = this // this指向调用call的对象,即我们要改变this指向的函数
return thisArg.fn(...args) // 执行函数并return其执行结果
}
但是我们有一些细节需要处理:
Function.prototype.myCall = function(thisArg, ...args) {
const fn = Symbol('fn') // 声明一个独有的Symbol属性, 防止fn覆盖已有属性
thisArg = thisArg || window // 若没有传入this, 默认绑定window对象
thisArg[fn] = this // this指向调用call的对象,即我们要改变this指向的函数
const result = thisArg[fn](...args) // 执行当前函数
delete thisArg[fn] // 删除我们声明的fn属性
return result // 返回函数执行结果
}
//测试
foo.myCall(obj) // 输出'前端脑洞'
4. 手写apply()
apply()
方法调用一个具有给定this值的函数,以及作为一个数组(或类似数组对象)提供的参数。 语法:func.apply(thisArg, [argsArray])
apply()
和call()
类似,区别在于call()
接收参数列表,而apply()
接收一个参数数组,所以我们在call()
的实现上简单改一下入参形式即可
Function.prototype.myApply = function(thisArg, args) {
const fn = Symbol('fn') // 声明一个独有的Symbol属性, 防止fn覆盖已有属性
thisArg = thisArg || window // 若没有传入this, 默认绑定window对象
thisArg[fn] = this // this指向调用call的对象,即我们要改变this指向的函数
const result = thisArg[fn](...args) // 执行当前函数(此处说明一下:虽然apply()接收的是一个数组,但在调用原函数时,依然要展开参数数组。可以对照原生apply(),原函数接收到展开的参数数组)
delete thisArg[fn] // 删除我们声明的fn属性
return result // 返回函数执行结果
}
//测试
foo.myApply(obj, []) // 输出'前端脑洞'
5. 手写bind()
bind()` 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。 语法: `function.bind(thisArg, arg1, arg2, ...)
从用法上看,似乎给call/apply包一层function就实现了bind():
Function.prototype.myBind = function(thisArg, ...args) {
return () => {
this.apply(thisArg, args)
}
}
但我们忽略了三点:
- bind()除了this还接收其他参数,bind()返回的函数也接收参数,这两部分的参数都要传给返回的函数
- new会改变this指向:如果bind绑定后的函数被new了,那么this指向会发生改变,指向当前函数的实例
- 没有保留原函数在原型链上的属性和方法
Function.prototype.myBind = function (thisArg, ...args) {
var self = this
// new优先级
var fbound = function () {
self.apply(this instanceof self ? this : thisArg, args.concat(Array.prototype.slice.call(arguments)))
}
// 继承原型上的属性和方法
fbound.prototype = Object.create(self.prototype);
return fbound;
}
//测试
const obj = { name: '前端脑洞' }
function foo() {
console.log(this.name)
console.log(arguments)
}
foo.myBind(obj, 'a', 'b', 'c')() //输出前端脑洞 ['a', 'b', 'c']
6. 手写一个防抖函数
防抖和节流的概念都比较简单,所以我们就不在“防抖节流是什么”这个问题上浪费过多篇幅了,简单点一下: 防抖,即短时间内大量触发同一事件,只会执行一次函数,实现原理为设置一个定时器,约定在xx毫秒后再触发事件处理,每次触发事件都会重新设置计时器,直到xx毫秒内无第二次操作,防抖常用于搜索框/滚动条的监听事件处理,如果不做防抖,每输入一个字/滚动屏幕,都会触发事件处理,造成性能浪费。
function debounce(func, wait) {
let timeout = null
return function() {
let context = this
let args = arguments
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(context, args)
}, wait)
}
}
7. 手写一个节流函数
防抖是延迟执行,而节流是间隔执行,函数节流即每隔一段时间就执行一次,实现原理为设置一个定时器,约定xx毫秒后执行事件,如果时间到了,那么执行函数并重置定时器,和防抖的区别在于,防抖每次触发事件都重置定时器,而节流在定时器到时间后再清空定时器
function throttle(func, wait) {
let timeout = null
return function() {
let context = this
let args = arguments
if (!timeout) {
timeout = setTimeout(() => {
timeout = null
func.apply(context, args)
}, wait)
}
}
}
实现方式2:使用两个时间戳prev旧时间戳和now新时间戳,每次触发事件都判断二者的时间差,如果到达规定时间,执行函数并重置旧时间戳
function throttle(func, wait) {
var prev = 0;
return function() {
let now = Date.now();
let context = this;
let args = arguments;
if (now - prev > wait) {
func.apply(context, args);
prev = now;
}
}
}
8. 数组扁平化
对于[1, [1,2], [1,2,3]]这样多层嵌套的数组,我们如何将其扁平化为[1, 1, 2, 1, 2, 3]这样的一维数组呢:
1.ES6的flat()
const arr = [1, [1,2], [1,2,3]]
arr.flat(Infinity) // [1, 1, 2, 1, 2, 3]
2.序列化后正则
const arr = [1, [1,2], [1,2,3]]
const str = `[${JSON.stringify(arr).replace(/(\[|\])/g, '')}]`
JSON.parse(str) // [1, 1, 2, 1, 2, 3]
3.递归 对于树状结构的数据,最直接的处理方式就是递归
const arr = [1, [1,2], [1,2,3]]
function flat(arr) {
let result = []
for (const item of arr) {
item instanceof Array ? result = result.concat(flat(item)) : result.push(item)
}
return result
}
flat(arr) // [1, 1, 2, 1, 2, 3]
4.reduce()递归
const arr = [1, [1,2], [1,2,3]]
function flat(arr) {
return arr.reduce((prev, cur) => {
return prev.concat(cur instanceof Array ? flat(cur) : cur)
}, [])
}
flat(arr) // [1, 1, 2, 1, 2, 3]
5.迭代+展开运算符
// 每次while都会合并一层的元素,这里第一次合并结果为[1, 1, 2, 1, 2, 3, [4,4,4]]
// 然后arr.some判定数组中是否存在数组,因为存在[4,4,4],继续进入第二次循环进行合并
let arr = [1, [1,2], [1,2,3,[4,4,4]]]
while (arr.some(Array.isArray)) {
arr = [].concat(...arr);
}
console.log(arr) // [1, 1, 2, 1, 2, 3, 4, 4, 4]
JS常见面试题,看看你都会多少?的更多相关文章
- vue.js常见面试题及常见命令介绍
Vue.js介绍 Vue.js是JavaScript MVVM(Model-View-ViewModel)库,十分简洁,Vue核心只关注视图层,相对AngularJS提供更加简洁.易于理解的API.V ...
- 前端JS常见面试题(代码自撸)
题目一示例: 适用于子数组等长度及不等长度. let arr = [ [1,2,3], [5,6,7,8], [9,10,11,12,13] ] function arrayDiagonal(arr) ...
- js常见面试题
1.大小写转化,将字符串转化成驼峰的方法 例:border-bottom-color转化为:borderBottomColor var str="border-bottom-color&qu ...
- JS常见面试题总结-真实被问到的!
1.判断数据类型有几种方法 console.log(typeof 'abc') // string console.log(Object.prototype.toString.call('abc')) ...
- 整理的最全 python常见面试题(基本必考)
整理的最全 python常见面试题(基本必考) python 2018-05-17 作者 大蛇王 1.大数据的文件读取 ① 利用生成器generator ②迭代器进行迭代遍历:for line in ...
- PHP常见面试题汇总(二)
PHP常见面试题汇总(二) //第51题:统计一维数组中所有值出现的次数?返回一个数组,其元素的键名是原数组的值;键值是该值在原数组中出现的次数 $array=array(4,5,1,2,3,1, ...
- 整理的最全 python常见面试题
整理的最全 python常见面试题(基本必考)① ②③④⑤⑥⑦⑧⑨⑩ 1.大数据的文件读取: ① 利用生成器generator: ②迭代器进行迭代遍历:for line in file; 2.迭代 ...
- 【javascript常见面试题】常见前端面试题及答案
转自:http://www.cnblogs.com/syfwhu/p/4434132.html 前言 本文是在GitHub上看到一个大牛总结的前端常见面试题,很多问题问的都很好,很经典.很有代表性.上 ...
- Vue常见面试题汇总
Vue框架常见面试题 1.active-class是哪个组件的属性?嵌套路由怎么定义? 答:vue-router模块的router-link组件. 2.怎么定义vue-router的动态路由?怎么 ...
随机推荐
- PyQt(Python+Qt)学习随笔:工具箱(QToolBox)编程使用的步骤及示例代码
老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 使用toolBox开发应用时,通过Designer设计ui界面时,只能在Designer中设计too ...
- PyQt学习随笔:QTableWidget的selectedRanges、setRangeSelected访问选中矩形范围的方法
老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 在QTableWidget对项的操作支持选中多个项的情况下,可以通过方法selectedRanges ...
- Proxy:简单小巧又强大好用的代理系统
之前的文章介绍了许多我们在用的DevOps相关的工具系统,例如:方便创建多套运行环境的Alodi,对运维友好的配置中心Kerrigan,强大的自定义任务引擎Probius以及专注于数据库自动化的ove ...
- element ui中循环出来的表格勾选问题
需求是这样的,一个房主屋里面有多个电表,每一个表是一个账户,一次只能给一个账户缴费,在点击go按钮进行缴费,这个时候判断是否跨表勾选,跨表格勾选则弹窗提示,反之符合需求,走缴费逻辑 上代码 <! ...
- 《图解TCP/IP》笔记
OSI参考模型 协议分层 为什么需要分层? 简化网络协议. 每一层只需要衔接上下层的服务. 利于模块化开发. 解耦. 分层的问题 过分模块化.提高数据处理的开销. OSI参考模型 作用及意义 将复杂的 ...
- Spring Boot 最简单的解决跨域问题
跨域问题(CORS) CORS全称Cross-Origin Resource Sharing,意为跨域资源共享.当一个资源去访问另一个不同域名或者同域名不同端口的资源时,就会发出跨域请求.如果此时另一 ...
- 腾讯健康码16亿亮码背后的Elasticsearch系统调优实践【>>戳文章免费体验Elasticsearch服务30天】
[活动]Elasticsearch Service免费体验馆>>Elasticsearch Service新用户特惠狂欢低至4折>>Elasticsearch Service企 ...
- Spark SQL 小文件问题处理
在生产中,无论是通过SQL语句或者Scala/Java等代码的方式使用Spark SQL处理数据,在Spark SQL写数据时,往往会遇到生成的小文件过多的问题,而管理这些大量的小文件,是一件非常头疼 ...
- Kali Linux破解wifi密码(无须外置网卡)
环境准备: 方式一(选择该方式):Kali Linux.笔记本一台.U盘(至少8G) 方式二:Kali Linux.外置网卡.笔记本一台.VM 特别说明,主要是使用方式一进行破解,如果有外置网 ...
- 出现VMware Workstation 无法连接到虚拟机。请确保您有权运行该程序、访问该程序使用的所有目录以及访问所有临时文件目录。 未能将管道连接到虚拟机: 所有的管道范例都在使用中。
今天在学习Linux 的时候 启动VM时出现了这个问题, 搞了很久终于弄好了, 就写篇博客来记录一下,帮助一下大家,如果对大家有帮助,还请大哥大姐点个关注,你的支持就是我坚持下去的动力 ! VMwar ...