本文中讲述到的面试题:
说说对闭包的认识, 它解决了什么问题
跨域问题有哪些处理方式
for…in 和 for…of的区别
new一个对象, 这个过程中发生了什么
js的防抖和节流是什么
数组中常用的方法有哪些
怎么判断一个object是否是数组
继承有哪些方式
说说js中call,apply,bind之间的关系
你了解promise吗
“请讲一下你对闭包的认识”——这道题几乎是前端面试必问的问题,今天我试着总结一下如何优雅的回答这道题
一句话解释:
能够读取其他函数内部变量的函数。
稍全面的回答:
在js中变量的作用域属于函数作用域, 在函数执行完后,作用域就会被清理,内存也会随之被回收,但是由于闭包函数是建立在函数内部的子函数, 由于其可访问上级作用域,即使上级函数执行完, 作用域也不会随之销毁, 这时的子函数(也就是闭包),便拥有了访问上级作用域中变量的权限,即使上级函数执行完后作用域内的值也不会被销毁。
这里涉及到对函数作用域的认识:js变量分为: 全局变量和局部变量;函数内部可以直接读取全局变量,而在函数外部自然无法读取函数内的局部变量;
原来 JavaScript 的闭包是这么回事这篇文职中作者详细的剖析了闭包.
可以通过下面的代码来帮助理解上面所说的:
function addCounter() {
let counter = 0
const myFunction = function () {
counter = counter + 1
return counter
}
return myFunction
}
const increment = addCounter()
const c1 = increment()
const c2 = increment()
const c3 = increment()
console.log('increment:', c1, c2, c3);
// increment: 1 2 3
在这段代码中increment
实际上就是闭包函数myFunction
, 它一共运行了三次,第一次的值是1,第二次的值是2,第三次的值是3。这证明了,函数addCounter
中的局部变量counter
一直保存在内存中,并没有在addCounter
调用后被自动清除。
在开发中, 其实我们随处可见闭包的身影, 大部分前端 JavaScript 代码都是“事件驱动”的,即一个事件绑定的回调方法; 发送ajax请求成功|失败的回调;setTimeout的延时回调;或者一个函数内部返回另一个匿名函数,这些都是闭包的应用。
下面是具体应用的栗子:
- 老掉牙的取正确值问题
for (var i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i) //10个10
}, 1000)
}
怎么取到每一次循环的正确值呢? 闭包这样用:
for (var i = 0; i < 10; i++) {
((j) => {
setTimeout(function () {
console.log(j) //1-10
}, 1000)
})(i)
}
声明了10个自执行函数,保存当时的值到内部
2.使用闭包模拟私有变量
私有变量在java里使用private声明就可以了, 但是在js中还没有,但是我们可以使用闭包模拟实现。
var counter = (function () {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val
}
return {
increment: function () {
changeBy(1)
},
decrement: function () {
changeBy(-1)
},
value: function () {
return privateCounter
}
}
})();
counter.value() //0
counter.increment() //1
counter.increment() //2
counter.decrement() //1
匿名函数已经定义就立即执行, 创建出一个词法环境包含counter.increment
、counter.decrement
、counter.value
三个方法,还包含了两个私有项:privateCounter
变量和changeBy
函数。这两个私有项无法在匿名函数外部直接访问,必须通过匿名包装器返回的对象的三个公共函数访问。
通常为了减轻web服务器的负载,我们把js
、css
,img
等静态资源分离到另一台独立域名的服务器上,在html页面中再通过相应的标签从不同域名下加载静态资源,而被浏览器允许,基于此原理,我们可以通过动态创建script,再请求一个带参网址实现跨域通信。
<script>
var script = document.createElement('script');
script.type = 'text/javascript';
// 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
script.src = 'http://www.daxihong.com:8080/login?user=admin&callback=jsonCallback';
document.head.appendChild(script);
// 回调执行函数
function jsonCallback(res) {
alert(JSON.stringify(res));
}
</script>
服务器端返回如下(返回即执行全局函数)
jsonCallback({
"status": 0, "user": "admin"})
$.ajax({
url: 'http://www.domain2.com:8080/login',
type: 'get',
dataType: 'jsonp', // 请求方式为jsonp
jsonpCallback: "handleCallback", // 自定义回调函数名
data: {
}
});
CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)跨域资源共享 CORS 详解。看名字就知道这是处理跨域问题的标准做法。CORS有两种请求,简单请求和非简单请求。
只要同时满足以下两大条件,就属于简单请求:
HEAD
GET
POST
如果是简单请求, 后端处理即可, 前端什么也不用干; 这里注意的是如果前端要带cookie, 前端也需要单独设置
var xhr = new XMLHttpRequest();
// 前端设置是否带cookie
xhr.withCredentials = true;
...
$.ajax({
...
xhrFields: {
withCredentials: true // 前端设置是否带cookie
},
crossDomain: true, // 会让请求头中包含跨域的额外信息,但不会含cookie
...
});
axios.defaults.withCredentials = true
可以借助koa2-cors
快速实现
const path = require('path')
const Koa = require('koa')
const koaStatic = require('koa-static')
const bodyParser = require('koa-bodyparser')
const router = require('./router')
const cors = require('koa2-cors')
const app = new Koa()
const port = 9871
...
// 处理cors
app.use(cors({
origin: function (ctx) {
return 'http://localhost:9099'
},
credentials: true,
allowMethods: ['GET', 'POST', 'DELETE'],
allowHeaders: ['t', 'Content-Type']
}))
// 路由
app.use(router.routes()).use(router.allowedMethods())
// 监听端口
...
跨域原理: 同源策略是浏览器的安全策略, 不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议, 不会执行js脚本, 不需要检验同源策略,也就不存在跨域问题。
实现思路:通过起一个代理服务器, 实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头cookie中域名,实现当前域下cookie的写入
利用node + webpack + webpack-dev-server代理接口跨域。在开发环境下,由于vue渲染服务和接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域,无须设置headers跨域信息了。后台可以不做任何处理。
webpack.config.js
部分配置
module.exports = {
entry: {
},
module: {
},
...
devServer: {
historyApiFallback: true,
proxy: [{
context: '/login',
target: 'http://www.daxihong.com:8080', // 代理跨域目标接口
changeOrigin: true,
secure: false, // 当代理某些https服务报错时用
cookieDomainRewrite: 'www.daxihong.com' // 可以为false,表示不修改
}],
noInfo: true
}
}
和使用node中间件跨域原理相似。前端和后端都不需要写额外的代码来处理, 只需要配置一下Ngnix
server{
# 监听9099端口
listen 9099;
# 域名是localhost
server_name localhost;
#凡是localhost:9099/api这个样子的,都转发到真正的服务端地址http://localhost:9871
location ^~ /api {
proxy_pass http://localhost:9871;
}
}
对于跨域还有挺多方式可以实现, 这里就不一一列举了。
var obj = new Object("name","sansan");
使用场景:
简单的防抖(debounce)代码:
function debounce(fn, wait) {
var timeout = null;
return function () {
if (timeout !== null) clearTimeout(timeout)
timeout = setTimeout(fn, wait)
}
}
// 处理函数
function handle() {
console.log(Math.random())
}
//滚动事件
window.addEventListener('scroll', debounce(handle, 2000));
function throttle(func, delay) {
var prev = Date.now();
return function () {
var context = this;
var args = arguments;
var now = Date.now();
if (now - prev >= delay) {
func.apply(context, args);
prev = Date.now();
}
}
}
function handle() {
console.log(Math.random());
}
window.addEventListener('scroll', throttle(handle, 2000));
区别:
函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。 比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。
开发中数组的使用场景非常多, 这里就简单整理总结一些常用的方法;从改变原有数据的方法、不改变原有数组的方法以及数据遍历的方法三方面总结。
let a = [1, 2, 3, 4, 5, 6, 7];
let item = a.splice(0, 3); // [1,2,3]
console.log(a); // [4,5,6,7]
// 从数组下标0开始,删除3个元素
let item1 = a.splice(0,3,'添加'); // [4,5,6]
console.log(a); // ['添加',7]
// 从数组下标0开始,删除3个元素,并添加元素'添加'
var array = [10, 1, 3, 4,20,4,25,8];
// 升序 a-b < 0 a将排到b的前面,按照a的大小来排序的
array.sort(function(a,b){
return a-b;
});
console.log(array); // [1,3,4,4,8,10,20,25];
// 降序
array.sort(function(a,b){
return b-a;
});
console.log(array); // [25,20,10,8,4,4,3,1];
pop() 删除一个数组中的最后的一个元素
shift() 删除数组的第一个元素
push() 向数组的末尾添加元素
unshift()向数组开头添加元素
reverse()
let a = [1,2,3];
a.pop(); // 3, 返回被删除的元素
console.log(a); // [1,2]
a.shift(); // 1
console.log(a); // [2]
a.push("末尾添加"); // 2 ,返回数组长度
console.log(a) ; [2,"末尾添加"]
a.unshift("开头添加"); // 3
console.log(a); //["开头添加", 2, "末尾添加"]
a.reverse(); // ["末尾添加", 2, "开头添加"]
console.log(a) // ["末尾添加", 2, "开头添加"]
let a = ['zhang', 'wang', 'zhou', 'wu', 'zheng'];
// 1位置开始被替换, 2位置开始读取要替换的 5位置前面停止替换
a.copyWithin(1, 2, 5);
// ["zhang", "zhou", "wu", "zheng", "zheng"]
['a', 'b', 'c'].fill(7)
// [7, 7, 7]
['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']
以上是9种会改变原数组的方法, 接下来是6种常用的不会改变原数组的方法
let a= ['hello','world'];
let str2=a.join('+'); // 'hello+world'
let a = [1, 2, 3];
let b = [4, 5, 6];
//连接两个数组
let newVal=a.concat(b); // [1,2,3,4,5,6]
let a = [2, 3, 4, 5]
let b = [ 4,...a, 4, 4]
console.log(a,b);
//[2, 3, 4, 5] [4,2,3,4,5,4,4]
let a=['啦啦',2,4,24,NaN]
console.log(a.indexOf('啦')); // -1
console.log(a.indexOf('啦啦')); // 0
- indexOf方法不能识别NaN
- indexOf方法检查是否包含某个值不够语义化,需要判断是否不等于-1,表达不够直观
let a=['OB','Koro1',1,NaN];
a.includes(NaN); // true 识别NaN
a.includes('Koro1',100); // false 超过数组长度 不搜索
a.includes('Koro1',-3); // true 从倒数第三个元素开始搜索
字符串也有一个slice() 方法是用来提取字符串的,不要弄混了。
let a = [{
name: 'OBKoro1'}, {
name: 'zhangsan'}];
let b = a.slice(0,1);
console.log(b, a);
// [{"name":"OBKoro1"}] [{"name":"OBKoro1"}]
a[0].name='改变原数组';
console.log(b,a);
// [{"name":"改变原数组"}] [{"name":"改变原数组"}]
1.无法中途退出循环,只能用return退出本次回调,进行下一次回调.
2.它总是返回 undefined值,即使你return了一个值。
如果数组中检测到有一个元素不满足, 则整个表达式返回false,且元素不会再进行检测
function isBigEnough(element, index, array) {
return element >= 10; // 判断数组中的所有元素是否都大于10
}
[12, 5, 8, 130, 44].every(isBigEnough); // false
[12, 54, 18, 130, 44].every(isBigEnough); // true
// 接受箭头函数写法
[12, 5, 8, 130, 44].every(x => x >= 10); // false
[12, 54, 18, 130, 44].every(x => x >= 10); // true
如果有一个元素满足条件,则表达式返回true, 剩余的元素不会再执行检测
filter 过滤原始数组,返回新数组
map 对数组中的每个元素进行处理,返回新的数组
reduce 为数组提供累加器,合并为一个值
reduce() 方法对累加器和数组中的每个元素(从左到右)应用一个函数,最终合并为一个值。
// 数组求和
let sum = [0, 1, 2, 3].reduce(function (a, b) {
return a + b;
}, 0);
// 6
// 将二维数组转化为一维 将数组元素展开
let flattened = [[0, 1], [2, 3], [4, 5]].reduce(
(a, b) => a.concat(b),
[]
);
// [0, 1, 2, 3, 4, 5]
这两个方法都可以识别NaN,弥补了indexOf的不足.
[1, 4, -5, 10,NaN].find((n) => Object.is(NaN, n));
// 返回元素NaN
[1, 4, -5, 10].findIndex((n) => n < 0);
// 返回索引2
for (let index of ['a', 'b'].keys()) {
console.log(index);
}
// 0
// 1
for (let elem of ['a', 'b'].values()) {
console.log(elem);
}
// 'a'
// 'b'
for (let [index, elem] of ['a', 'b'].entries()) {
console.log(index, elem);
}
// 0 "a"
// 1 "b"
使用 Object.prototype.toString 来判断是否是数组
function isArray(obj){
return Object.prototype.toString.call( obj ) === '[object Array]';
}
这里使用call来使 toString 中 this 指向 obj。进而完成判断
使用 原型链 来完成判断
function isArray(obj){
return obj.__proto__ === Array.prototype;
}
基本思想: 实例如果是某个构造函数构造出来的那么 它的
__proto__
是指向构造函数的prototype
属性
利用JQuery, 利用JQuery isArray 的实现其实就是方法1。
function isArray(obj){
return $.isArray(obj)
}
简单介绍一下前两种方式, 后面几种继承方式大家可以自行上网查找.
class Animal {
constructor(name) {
this.name = name;
};
eat() {
console.log(this.name + '正在吃东西');
};
}
//继承动物类
class Cat extends Animal {
catchMouse(){
console.log(`${
this.name}正在捉老鼠`);
}
}
var cat= new Cat('Tom猫');
cat.catchMouse();// Tom猫正在捉老鼠
细心的同学可能会发现, 在Cat 类中没有构造函数, 这里有一个小的知识点,就是ES6的继承方法中如果子类没有写构造函数的话就一般默认添加构造。举个例子。
class Cat extends Animal{
}
// 等同于
class Cat extends Animal{
constructor(name) {
super(name);
//super作为函数调用时,代表父类的构造函数。
}
}
注意:如果我写了构造函数但是没有写super的话,或者super方法的参数不对等等,编译器都会报错。
在ES6之前,也有很多继承的方法,其中一个很常用的方法就是使用原型继承。其基本方法就是一个父类的实例赋值给子类的原型。这个继承方式是通过__proto__
建立和子类之间的原型链,当子类的实例需要使用父类的属性和方法的时候,可以通过__proto__
一级级向上找;
function Animal(name) {
this.name = name;
}
Animal.prototype.eat= function () {
console.log(this.name + '正在吃东西')
};
function Cat(furColor){
this.furColor = furColor ;
};
Cat.prototype = new Animal();
let tom = new Cat('black');
console.log(tom)
缺点: 1. 子类实例时,无法向父类构造函数传参。
2. 父类的私有属性被所有实例共享
这又是一个面试经典问题, 也是ES5中众多坑中的一个,在 ES6 中可能会极大避免 this 产生的错误,但是为了一些老代码的维护,最好还是了解一下 this 的指向和 call、apply、bind 三者的区别.
bind,apply,call三者都可以用来改变this
的指向, 下面分别对他们进行比较分析:
var a ={
name : "Cherry",
fn : function (a,b) {
console.log( a + b)
}
}
var b = a.fn;
b.apply(a,[1,2]) // 3
b.call(a, 4,5,6) // 15
我们常常使用的验证是否是数组(前提是toString()方法没有被重写过):
function isArray(obj){
return Object.prototype.toString.call(obj) === '[object Array]' ;
}
var a ={
name : "Cherry",
fn : function (a,b) {
console.log( a + b)
}
}
var b = a.fn;
b.bind(a,1,2)(); // 3
我们发现bind()
方法还需要调用一次; 是由于 bind()
方法创建一个新的函数,我们必须手动去调用。
this
的指向前端面试过程中,基本都会问到 Promise,如果你足够幸运,面试官问的比较浅,仅仅问 Promise 的使用方式,那么恭喜你。事实上,大多数人并没有那么幸运, 很多面试官在promise这块都是由浅入深的提问.
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理,让开发者不用再关注于时序和底层的结果。Promise的状态具有不受外界影响和不可逆两个特点。
Promise解决了回调地狱的问题, 提高代码的可读性以及解决信任度问题.
传统的回调有五大信任问题:
ES6规定,Promise对象是一个构造函数,用来生成Promise实例。
下面代码创造了一个Promise实例。
var promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve
和reject
。它们是两个函数,由JavaScript引擎提供,不用自己部署。
resolve
函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从Pending
变为Resolved
),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject
函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从Pending
变为Rejected
),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
Promise 实例具有then
方法,也就是说,then
方法是定义在原型对象Promise.prototype
上的。它的作用是为 Promise 实例添加状态改变时的回调函数。then
方法的第一个参数是resolved
状态的回调函数,第二个参数(可选)是rejected
状态的回调函数。
Promise.prototype.catch
方法是.then(null, rejection)
或.then(undefined, rejection)
的别名,用于指定发生错误时的回调函数。
getJSON('/posts.json').then(function(posts) {
// ...
}).catch(function(error) {
// 处理 getJSON 和 前一个回调函数运行时发生的错误
console.log('发生错误!', error);
});
上面代码中,getJSON
方法返回一个 Promise 对象,如果该对象状态变为resolved
,则会调用then
方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected
,就会调用catch
方法指定的回调函数,处理这个错误。
Promise.all
方法用于将多个 Promise 实例,包装成一个新的 Promise 实例, 返回最先执行结束的 Promise 任务的结果,不管这个 Promise 结果是成功还是失败。
Promise.race
方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。如果全部成功执行,则以数组的方式返回所有 Promise 任务的执行结果。 如果有一个 Promise 任务 rejected
,则只返回 rejected
任务的结果。
var promise = new Promise((resolve, reject)=>{
console.log('我是promise任务');
resolve('resolved')
})
promise.then(res =>{
console.log(res)
})
console.log("我是同步任务");
setTimeout(()=>{
console.log("我是延时任务");
}, 0)
上面代码的执行顺序是: 我是promise任务、我是同步任务、resolved、我是延时任务。
Promise 新建后立即执行,立即 resolve
的 Promise 对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时;setTimeout在下一轮“事件循环”开始时执行。
在面试中, 很多问题并没有真正的答案,至于知识点能掌握到什么样的程度,都需要靠自己不断的学习积累, 在开发中不断的使用也是加深对知识点理解的方式。由于个人精力有限,只是针对一些常遇到的面试题,做了一些浅显的答案解析,希望对大家有所帮助吧。
欢迎大家关注我的公众号——程序员成长指北。请自行微信搜索——“程序员成长指北”
上一篇文章:
web前端高频面试题–css篇