面试题整理
70个JavaScript面试题集锦,内含解答,自测 JS 掌握程度
1. ['1', '2', '3'].map(parseInt)输出结果
['1', '2', '3'].map(parseInt); // [1,NaN,NaN]
这道题需要理解两个点:
①.parseInt(string, radix)
接收两个参数,第一个表示被处理的值(字符串),第二个表示为解析时的基数。
radix 可选。表示要解析的数字的基数。该值介于 2 ~ 36 之间。如果省略该参数或其值为 0,则数字将以 10 为基础来解析。如果它以 “0x” 或 “0X” 开头,将以 16 为基数。如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN。
②.代码实际执行的以下代码
['1', '2', '3'].map((item, index) => { return parseInt(item, index) }) parseInt('1', 0) // 1 parseInt('2', 1) // NaN parseInt('3', 2) // NaN, 3 不是二进制 string要符合基数的规范
同样的经典题
['10','10','10','10','10'].map(parseInt) // => parseInt('10', 0) // 10 parseInt('10', 1) // NaN parseInt('10', 2) // 2, parseInt('10', 3) // 3, parseInt('10', 4) // 4, // 所以['10','10','10','10','10'].map(parseInt) => [10,NaN,2,3,4];
2.实现一个函数:输入一个整数,求该整数的二进制表达中有多少个1?
方法1:
function fun1(num){ return new Number(num).toString(2).split('').filter(item => item == 1).length } fun1(9) // 2
方法2:
function fun2(num){ var len = 0; while(num > 0) { var a = num % 2; num = (num - a) / 2; if(a === 1) { len++; } } return len; } fun2(9)
3.实现一个函数:给定一个字符串,找出出现次数最多的字符及次数
function getMaxStr(str) { let json = {}; for(let i =0;i<str.length;i++){ if(json[str[i]]) { json[str[i]]++ }else { json[str[i]] = 1; } } let maxNum = 0; let maxStr = ''; for(let key in json){ if(json[key] > maxNum){ maxNum = json[key]; maxStr = key; } } console.log(`出现次数最多的字符是${maxStr},出现次数是${maxNum}`) } getMaxStr('ddddssfes') // 出现次数最多的字符是d,出现次数是4
4.this指向理解
var a = 1; var obj = { a: 2, func1: ()=>{console.log(this.a)}, func2: function(){console.log(this.a)} } var obj2 = {a: 3}; console.log(obj.func1()) // 1 console.log(obj.func2()) // 2 obj.func2.apply(obj2) // 3 var newFunc = obj.func2; newFunc() //1
tips: 函数直接被调用时this则指向window,函数作为某对象的方法调用时,this指向该对象,而箭头函数没有执行上下文,取决于他就近的外面的一层非箭头函数的函数。
箭头函数的this看外层的是否有函数,如果有,外层函数的this就是内部箭头函数的this,如果没有,则this是window
5.下面三段代码分别输出什么?并且什么时候输出什么?
for (var i = 0; i < 5; i++) { setTimeout(function () { console.log(i) }, 1000 * i) } // 在极短的一段时间内输出5,随后每隔一秒输出一个5 // 5 5 5 5 5
// for循环结束后才到宏观任务setTimeout,这时候i的值已经变为5了
for (let i = 0; i < 5; i++) { setTimeout(function () { console.log(i) }, 1000 * i) } // 在极短的一段时间内输出0,随后每隔一秒结果加1 // 0 1 2 3 4
// let i 是块作用域
for (var i = 0; i < 5; i++) { (function (i) { setTimeout(function () { console.log(i) }, 1000 * i) })(i) } // 在极短的一段时间内输出0,随后每隔一秒结果加1 // 0 1 2 3 4
// 立即执行函数,创建了属于自己的作用域,因此每一次执行都是不同的i
6.自由变量理解
var x = 10 function fn() { console.log(x) } function show(f) { var x = 20 (function() { f() //10,而不是20 })() } show(fn)
什么是自由变量? --当前作用域没有定义的变量,这成为自由变量
var a = 100 function fx() { var b = 200 console.log(a) // 这里的a在这里就是一个自由变量 console.log(b) } fx()
在fn函数中,取自由变量x的值时,要到哪个作用域中取?--要到创建fn函数的那个作用域中取,无论fn函数将在哪里调用。
要到创建这个函数的那个域。作用域中取值,这里强调的是“创建”,而不是“调用”,切记切记——其实这就是所谓的"静态作用域"。
7. 闭包的理解
function outer(){ var num=0;//内部变量 return function add(){//通过return返回add函数,就可以在outer函数外访问了 num++;//内部函数有引用,作为add函数的一部分了 console.log(num); }; } var func1=outer();
func1();//实际上是调用add函数, 输出1 func1();//输出2 因为outer函数内部的私有作用域会一直被占用 var func2=outer(); func2();// 输出1 每次重新引用函数的时候,闭包是全新的。 func2();// 输出2
一般情况下,函数执行会形成一个新的私有的作用域,当私有作用域中的代码执行完成后,我们当前作用域都会主动的进行释放和销毁。但当遇到函数执行返回了一个引用数据类型的值,并且在函数的外面被一个其他的东西给接收了,这种情况下一般形成的私有作用域都不会销毁。
所谓内存泄漏指任何对象在您不再拥有或需要它之后仍然存在。闭包不能滥用,否则会导致内存泄露,影响网页的性能。闭包使用完了后,要立即释放资源,将引用变量指向null。
8.手写简化版防抖函数、节流函数
// 防抖函数 防抖函数原理:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。 只执行最后提交的一次 const debounce = (fn, delay = 500) => { let timer = null; return () => { clearTimeout(timer); timer = setTimeout(() => { fn(); }, delay); }; }; // 节流函数 防抖函数原理:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。 const throttle = (fn, delay = 500) => { let flag = true; return () => { if (!flag) return; flag = false; setTimeout(() => { fn(); flag = true; }, delay); }; };
9.说出下面运行的结果,解释原因
function test(person) { person.age = 26; person = { name: 'hzj', age: 18 }; return person; } const p1 = { name: 'fyq', age: 19 }; const p2 = test(p1); console.log(p1); //{name: “fyq”,age: 26} console.log(p2); //{name: “hzj”,age: 18}
原因: 在函数传参的时候传递的是对象在堆中的内存地址值,test函数中的实参person是p1对象的内存地址,通过调用person.age = 26确实改变了p1的值,但随后person变成了另一块内存空间的地址,并且在最后将这另外一份内存空间的地址返回,赋给了p2。
10.new 关键字有什么作用
new
关键字与构造函数一起使用以创建对象在JavaScript中。
下面看看例子:
function Employee(name, position, yearHired) { // creates an empty object {} // then assigns the empty object to the "this" keyword // this = {}; this.name = name; this.position = position; this.yearHired = yearHired; // inherits from Employee.prototype // returns the "this" value implicitly if no // explicit return statement is specified }; const emp = new Employee("Marko Polo", "Software Developer", 2017);
new
关键字做了4
件事:
-
创建空对象
{}
-
将空对象分配给
this
值 -
将空对象的
__proto__
指向构造函数的prototype
-
如果没有使用显式
return
语句,则返回this
根据上面描述的,它将首先创建一个空对象{},然后它将this值赋给这个空对象this={},并向这个对象添加属性。因为我们没有显式的return语句,所以它会自动为我们返回this。
11. http常见的响应码
常见状态码:
- 200 请求成功
- 301 永久重定向,一般是地址发生变化
- 302 临时重定向
- 304 Not Modified(通常是在协商缓存中表示本次内容未修改)
- 400 Bad Request - 前端请求错误
- 401 Unauthorized - 未授权
- 403 Forbidden - 请求被拒绝
- 404 Not Found - 资源不存在
- 500 Inter Server Error - 服务端异常
- 503 Server Unavailable - 服务不可用
12.前端鉴权
鉴权主要分为四种:
- HTTP Basic Authentication (HTTP基本认证)
- session-cookie
- Token 验证(包括JWT,SSO)
- OAuth(开放授权)
我们普通网站常用的认证就是session-cookie的方式,用户向服务端发生请求,服务端会创建session并保存相关身份信息,并向客户端下发一个sessionId,大家如果用心的话,会发现跟JAVA交互的时候,浏览器会有一个JSESSION_ID,跟PHP交互的时候,会有一个PHPSESSION_ID;后面的每次请求,客户端都会自动带上这个cookie跟服务端通信。
实际上大家要明白每一种方式的作用;SSO主要用来做单点登录;OAuth主要用来做第三方网站授权;JWT就是一种便于扩展的跨域认证解决方案,通常会考察这个。
OAuth2.0原理
讲原理主要是让你对这个过程做梳理,并不要求对源码过程做剖析,所以可以通过一个简单的流程来进行回答。我通过微信的授权登录来给大家做讲解:OAuth2.0是一个开源的授权认证方案。当我们登录一个网站时,如果想要通过微信做授权登录,从而获取微信的用户信息,正常情况肯定是不允许,通过微信开放的OAuth2.0我们可以做授权认证。我们点击自己网站的微信按钮,跳转一个链接,这个链接比如是:https://open.weixin.qq.com/connect/oauth2/authorize?
会跳转到微信那边去让用户同意授权,用户同意以后,会重定向回来并携带一个code,此code是微信下发的临时凭证。开发者拿到此code以后,就可以获取access_token,根据下发的token,我们才能有权限获取其它接口信息。
- 点击按钮,跳转第三方授权网站
- 用户同意授权,重定向回来携带code
- 开发者根据code获取微信access_token
- 拿到token拉取用户资料
回答的时候,能够把这个过程描述清楚就好,不要过多解析源码。更进一步了解OAuth2.0可参考阮一峰教程:http://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html
OAuth2.0的四种方式
- 授权码(最常用,适用于有后端的web)
- (授权码)隐藏式 (适用于没有后端的web,少了拿授权码步骤,直接拿到令牌)
- 密码式 (通过用户给出用户名及密码获取令牌,风险大,在其他授权方式无法获取到令牌且高度信任该应用时下适用)
- 客户端凭证 (适用于没有前端的命令行应用,这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,即有可能多个用户共享同一个令牌。)
授权码图示
(授权码)隐藏式图示
A 网站拿到令牌以后,就可以向 B 网站的 API 请求数据了。
此时,每个发到 API 的请求,都必须带有令牌。具体做法是在请求的头信息,加上一个Authorization
字段,令牌就放在这个字段里面。
更新令牌
令牌的有效期到了,如果让用户重新走一遍上面的流程,再申请一个新的令牌,很可能体验不好,而且也没有必要。OAuth 2.0 允许用户自动更新令牌。
具体方法是,B 网站颁发令牌的时候,一次性颁发两个令牌,一个用于获取数据,另一个用于获取新的令牌(refresh token 字段)。令牌到期前,用户使用 refresh token 发一个请求,去更新令牌。
未完,待续...