综合类

82 阅读18分钟

综合题目

1.讲讲输入完网址按下回车,到看到网页这个过程中发生了什么

  1. 域名解析(DNS递归一层一层往里找,迭代)域名->ip地址

  2. 发起TCP的三次握手

  3. 建立TCP连接后发起http请求

  4. 服务器端响应http请求,发送响应数据给客户端(在这个过程中如果解析响应头发现为301、302,那么就会从响应头的LOCATION字段读取重定向的地址,然后再发起新的请求)

  5. 浏览器接收到请求的资源,解析资源。

  6. 对页面进行渲染

    页面渲染

    1. 构建DOM树---把HTML转换为浏览器能够理解的结构
    2. 样式计算---把CSS文本转换为浏览器能够理解的结构
    3. 布局---构建布局树LayoutTree,所有DOM树中的节点加到布局树中
    4. 图层---构建图层树LayerTree
    5. 生成绘制列表
    6. 生成图块并栅格化
    7. 合成和显示

2.谈谈你对前端性能优化的理解

  1. 请求数量:合并脚本和样式表,CSS Sprites,拆分初始化负载,划分主域
  2. 请求带宽:开启GZip,精简JavaScript,移除重复脚本,图像优化,将icon做成字体
  3. 缓存利用:使用CDN,使用外部JavaScript和CSS,添加Expires头,减少DNS查找,配置ETag,使AjaX可缓存
  4. 页面结构:将样式表放在顶部,将脚本放在底部,尽早刷新文档的输出
  5. 代码校验:避免CSS表达式,避免重定向

3.浏览器本地存储

在HTML5中提供了sessionStoragelocalStorage

sessionStorage用于本地存储一个会话(session)中的数据,这些数据只有在同一个会话中的页面才能访问并且当会话结束后数据也随之销毁,是会话级别的存储。即打开页面被创建,关闭页面即销毁。适用于一次性登陆页面,考虑安全性。

localStorage用于持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的。缺点是只能存字符串,不能存对象,不能像cookie一样设置过期时间。

不同浏览器之间两者信息都不可共享

相同浏览器的不同页面可以共享相同的localStorage

但是不同页面或标签页间无法共享sessionStorage的信息,当如果一个标签页包含多个iframe标签且他们属于同源页面时可以共享。

同源必须协议、域名(ip)、端口号都相同。

API:

localStorage.setItem("key","value");//以“key”为名称存储一个值“value”localStorage.getItem("key");//获取名称为“key”的值localStorage.removeItem("key");//删除名称为“key”的信息。localStorage.clear(); //清空localStorage中所有信息

其他的本地缓存方式:

cookie为了辨别身份而存在用户本地终端上的数据,由键值对组成。

indexedDB是一种低级API,用于客户端存储大量结构化数据。

4.请你谈谈Cookie的弊端

  1. 每个特定的域名下最多生成的cookie个数有限制
  2. IE和Opera 会清理近期最少使用的cookie,Firefox会随机清理cookie
  3. cookie的最大大约为4096字节,为了兼容性,一般不能超过4095字节
  4. 安全性问题。如果cookie被人拦截了,那人就可以取得所有的session信息。

5.MVVM模型的理解

MVVMModel-View-ViewModel 的缩写,它是一种基于前端开发的架构模式,其核心是提供对ViewViewModel的双向数据绑定,这使得ViewModel 的状态改变可以自动传递给View,即所谓的数据双向绑定。

MVVMMVC的升级版,MVCModel-View-Controller 的缩写。

View :视图-把数据以某种方式呈现给用户

Model:模型-管理数据

Controller :响应用户操作,并将Model 更新到View 上。

随着技术的更新,大量调用相同API,操作复杂、冗余,影响页面性能、用户体验,Model和View之间的数据同步及更新越来越多、复杂。

MVVM出现解决了以上的问题,

Model-数据层

View-视图层

ViewModel-模型视图

ViewModel 即MVVM模型的核心层,通过数据的双向绑定把 View层和 Model层连接了起来,具体的操作是通过数据绑定把数据转化为看到的页面;通过DOM时间监听把页面转化为数据。

保证视图和数据的一致性,这种轻量级的架构让前端开发更加高效、便捷。

优点:1、主要目的是为了分离视图和模型。2、降低代码耦合,提高代码和逻辑的耦合性。3、方便代码的测试。

6.一个页面上有大量的图片(大型电商网站),加载很慢,你有哪些方法优化这些图片的加载,给用户更好的体验[性能优化]。

  1. 图片懒加载,滚动到相应位置才加载图片。
  2. 图片预加载,如果为幻灯片、相册等,将当前展示图片的前一张和后一张优先下载。
  3. 使用CSSspriteSVGspriteIconfontBase64等技术,如果图片为css图片的话。
  4. 如果图片过大,可以使用特殊编码的图片,加载时会先加载一张压缩的特别厉害的缩略图,以提高用户体验。

7.如何理解前端模块化?

模块化就是将一个复杂的工程项目分解成多个实现不同功能的独立模块,从而实现代码的高可复用性及可维护性,但也因此带来一些问题,如模块之间相互依赖等关系,需要一个统一的标准,于是有了commonJSAMD等标准,还有项目的打包工具webpack

8.get和post请求的区别?

两者都是http协议中两种发送请求的方法,而http是基于TCP/IP的数据在网络中通信的协议标准。

  • get是从服务器上获取数据,post是从服务器传送数据。
  • get提交的数据会放在URL之后,以?分割URL和传输数据,数据的参数之间用&分割;post方法是把要传送的数据放在HTTP包的body里。
  • get提交的数据会有大小限制,一般在2k字节以内,而post没有限制,但一般也不会超过80 100kb.
  • get提交数据时,存在的安全隐患较大,用户数据会显示在URL上
  • get是通过地址栏来取值,post是通过提交表单来传值。

9.Async和Await原理

asyncgenerator的语法糖

generator函数:

function * getAge() {
    yield 1;
    yield 2;
}
let gen = getAge();
console.log(gen.next());//{ value: 1, done: false }
console.log(gen.next());//{ value: 2, done: false }
console.log(gen.next());//{ value: undefined, done: true }

1.加星号

2.yield控制代码执行,对象每执行一次next()方法,向下执行一次

3.若要对next()传参,则第一次执行的next()函数永远是undefined,见下例

function * getAge() {
    let a = yield 1
    console.log(a)
    yield 2
}
​
let gen = getAge()
// console.log(gen.next());
// console.log(gen.next(444));  //此时console.log(a)输出444
​
console.log(gen.next(444));
console.log(gen.next()); // 此时console.log(a)输出undefined,实际上第一个next只对a进行了声明,并未赋值
//整体输出:
// { value: 1, done: false }
// undefined
// { value: 2, done: false }

async函数对generator的改进:

1.内置执行器,不需要手动使用next()来执行。

?2.await命令后可以是promise对象或者原始类型的值,yield之后只能是promise对象或者Thunk函数。

3.async返回值为promise对象,返回非promise时会自动转为promise对象并返回。

总之,async函数的返回值是promise对象,把async函数看成多个异步操作,包装成了一个promise对象,await命令可以看成.then的语法糖。async函数就是声明一个异步函数,整体代码仍然同步执行,用事件循环来解释其原理时,可以理解为执行到async函数时,对于紧挨着await之后的代码作为宏任务直接执行,而await下一行的代码则放到微任务的任务队列中,等待宏任务执行完毕后再执行。

10.webpack工作原理

随着前端开发越来越复杂,规模越来越大,开始走向了工程化的独立开发,日常写的代码包括.js.html.css.json.png.scss等需要我们进行打包合并到一个文件里面,简单来说就是把这些文的件压缩、打包等一系列操作自动化,包括分析项目结构、配合loader处理ES6语法等特殊资源的加载和解析、通过plugin实现自动化操作。

整个过程包括两方面:

  • 通过 Loader 处理特殊类型资源的加载,例如加载样式、图片;
  • 通过 Plugin 实现各种自动化的构建任务,例如自动压缩、自动发布。

具体的流程:

  1. 初始化:从配置文件开始读取和合并参数,对参数进行整合,得到整合后的参数,实例化插件new Plugin()
  2. 开始编译:通过整合后的参数,初始化Complier对象,加载插件(调用插件中的apply方法),通过执行complie.run开始编译
  3. 确定入口:根据配置文件的entry找到入口文件
  4. 编译模块:从入口文件出发,调用配置的loader,对模块进行转换。具体的操作是根据入口模块开始依次递归找出所有依赖,形成依赖关系树,然后将递归结果交给各自的loader进行编译处理,得到chunk。
  5. 输出:模块转换完成后得到一个个的chunk代码块,并转换成文件输出。

11.webpack的loader和plugin区别?如何自己写一个plugin?

webpack本身只能打包commonjs规范的js的文件,因此引入Loader,Loader可以加载不同格式的资源文件,并对这些文件进行一些处理包括编译、压缩等,如将scss转换为css,或者typescript转化为js,loader运行在Nodejs中,仅仅为了打包各种资源文件。

如:

file-loader:文件加载

url-loader:文件加载,可以设置阈值,小于时把文件base64编码

image-loader:加载并压缩图片

babel-loader:ES6+转成ES5

ts-loader:将ts转成js

css-loader:处理@importurl这样的外部资源

style-loader:在head创建style标签把样式插入

eslint-loader:进行代码eslint检查

cache-loader:性能开销大的loader前添加,将结果缓存到磁盘

Plugin目的在于解决loader无法实现的其他事,也是对webpack的功能进行拓展,从打包优化和压缩,到重新定义环境变量,功能强大到可以用来处理构建过程中各种各样的任务。

如:

  1. ignore-plugin:忽略文件
  2. terser-webpack-plugin:支持压缩ES6(webpack4)
  3. webpack-arallel-uglify-plugin: 多进程执行代码压缩,提升构建速度
  4. mini-css-extract-plugin: 分离样式文件,css提取为独立文件,支持按需加载
  5. serviceworker-webpack-plugin: 为网页应用增加离线缓存功能
  6. clean-webpack-plugin: 目录清理
  7. ProviderPlugin: 自动加载模块,代替requireimport
  8. html-webpack-plugin可以根据模板自动生成html代码,并自动引用cssjs文件
  9. extract-text-webpack-pluginjs文件中引用的样式单独抽离成css文件
  10. compression-webpack-plugin 生产环境可以采用gzip压缩jscss
  11. happypack: 通过多进程模式,来加速代码构建

Loader运行在打包文件之前;

Plugins在整个编译周期都起作用

12.npm怎么发包?

npmjs的包管理工具,通过npm i xx下载模块,实现代码复用

注册:注册一个npm账号

初始化:本地创建一个文件夹z-tool,执行命令进入该文件夹 $ cd z-tool ,执行npm init初始化,完成后在文件中新建一个index.js的文件,写入内容

登陆:(第一次登陆)终端输入npm adduser,(不是第一次)终端输入npm login

发包:npm publish

?13.常见的跨域解决方案

跨域是浏览器的一种安全策略,跨域出现的原因是违反了同源策略,同源策略即

协议+主机+端口

?协议+ip+端口 或者

?协议+域名+端口

[?]  不一定对

三者相同,

若存在其中之一不同则非同源。常常出现在前后端分离的架构项目中

非同源会限制以下行为:

1.CookieLocalStorageIndexDB本地存储无法读取. 2.无法获得DOM元素和JS对象

3.js无法发起ajax请求

解决方案:

1. jsonp:只能发送get请求,而不能使用post请求,但是项目中大多都是post请求,一般不用。

简单来说就是动态创建<script>标签,然后利用<script>src不受同源策略约束来跨域获取数据,即<script>标签的src属性发送请求到服务器,服务器返回js代码,浏览器端接收响应,然后就直接执行了,和通过<script>标签引用外部文件的原理相同。

//原生实现
<script>
    var script = document.createElement('script');
    script.type = 'text/javascript';
​
    // 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
    script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';
    document.head.appendChild(script);
​
    // 回调执行函数
    function handleCallback(res) {
        alert(JSON.stringify(res));
    }
 </script>
​
//服务端返回结果(返回的时候就是执行全局函数--异步操作):
handleCallback({"status": true, "user": "admin"})
//vue实现
this.$http.jsonp('http://www.domain2.com:8080/login', {
    params: {},
    jsonp: 'handleCallback'
}).then((res) => {
    console.log(res); 
})

2. 跨域资源共享(CORS)

原理为在接口的response headers响应头上加上一个设置,设置允许前端跨域访问该接口(小项目推荐)

response.addHeader('Access-Control-Allow-Origin','*') // @CrossOrigin原理

普通的跨域请求只需要在服务端设置Access-Control-Allow-Origin即可,前端不需要加;若是要带cookie请求,前后端都要设置。

由于同源策略的限制,所读取的cookie为跨域请求接口所在域的cookie,而非当前页。如果想实现当前页cookie的写入,使用nginx反向代理接口实现跨域。

前端代码:

var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容// 前端设置是否带cookie
xhr.withCredentials = true;
​
xhr.open('post', 'http://www.domain2.com:8080/login', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('user=admin');
​
xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status == 200) {
        alert(xhr.responseText);
    }
};

服务端后台NodeJs代码,也可以是java代码

var http = require('http');
var server = http.createServer();
var qs = require('querystring');
​
server.on('request', function(req, res) {
    var postData = '';
​
    // 数据块接收中
    req.addListener('data', function(chunk) {
        postData += chunk;
    });
​
    // 数据接收完毕
    req.addListener('end', function() {
        postData = qs.parse(postData);
​
        // 跨域后台设置
        res.writeHead(200, {
            'Access-Control-Allow-Credentials': 'true',     // 后端允许发送Cookie
            'Access-Control-Allow-Origin': 'http://www.domain1.com',    // 允许访问的域(协议+域名+端口)
            /* 
             * 此处设置的cookie还是domain2的而非domain1,因为后端也不能跨域写cookie(nginx反向代理可以实现),
             * 但只要domain2中写入一次cookie认证,后面的跨域接口都能从domain2中获取cookie,从而实现所有的接口都能跨域访问
             */
            'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'  // HttpOnly的作用是让js无法读取cookie
        });
​
        res.write(JSON.stringify(postData));
        res.end();
    });
});
​
server.listen('8080');
console.log('Server is running at port 8080...');

3. nginx反向代理接口跨域

跨域原理:同源策略是只存在于浏览器中的安全策略,不是HTTP协议的一部分,所有当服务器端调用HTTP接口时只是使用HTTP协议,不会执行JS脚本,不存在同源策略,也不存在跨域问题。

实现思路:通过nginx配置一个代理服务器(要求域名要和domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。

nginx具体配置:

#proxy服务器
server {
    listen       81;
    server_name  www.domain1.com;
​
    location / {
        proxy_pass   http://www.domain2.com:8080;  #反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
        index  index.html index.htm;
​
        # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
        add_header Access-Control-Allow-Origin http://www.domain1.com;  #当前端只跨域不带cookie时,可为*
        add_header Access-Control-Allow-Credentials true;
    }
}

前端代码:

var xhr = new XMLHttpRequest();
​
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
​
// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();

服务端后台nodejs代码

var http = require('http');
var server = http.createServer();
var qs = require('querystring');
​
server.on('request', function(req, res) {
    var params = qs.parse(req.url.substring(2));
​
    // 向前台写cookie
    res.writeHead(200, {
        'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'   // HttpOnly:脚本无法读取
    });
​
    res.write(JSON.stringify(params));
    res.end();
});
​
server.listen('8080');
console.log('Server is running at port 8080...');

14.遍历数组的方法?

1.for循环

const arr = [2,2,3,4,4]
for(let i = 0 ; i < arr.length ; i++) {
    console.log(i) // 0,1,2,3,4,5
    console.log(arr[i]) // 2,2,3,4,4
}

2.forEach()

const arr = ['11', '22', '33', '44']
arr.forEach((value, key) => {
    console.log(value) // 11,22,33,44
    console.log(key) //1,2,3,4
})

传入一个函数,没有返回值

3.map()

const arr = [1, 2, 3, 4]
const arr2 = arr.map((value) => {
    return value * 2
})
console.log(arr2) // 返回一个数组[2,4,6,8]

4.for...of

const obj = [    'fan',    'lu',    'fa']
for(let value of obj) {
    console.log(value) //fan,lu,fa
}

不能直接遍历对象,他的作用是遍历一个可迭代的对象(即存在iterator属性)

如果不能使用for...of来遍历对象,则需要给对象添加一个迭代器属性,使他可以遍历任何数据结构的元素。

const obj = {
    name: 'fan',
    age: 23
}
obj[Symbol.iterator] = function() {
    let index = 0
    let keys = Object.keys(this) // this -> obj
    console.log(this)
    const _this = this
    return {
        next() {
            if(index < keys.length) {
                return {
                    value: _this[keys[index++]],
                    done: false
                } 
            } else {
                return {
                    done: true
                }
            } 
        }
    }
}
for(let itemKey of Object.keys(obj)) {
    console.log(itemKey) // name, age 
}
​
for(let itemValue of Object.values(obj)) {
    console.log(itemValue) // fan, 23
}
​
for(let objItem of obj) {
    console.log(objItem) // fan , 23
}

15.函数柯里化

高阶函数:以函数作为函数的输入或者输出的函数,将过程逻辑抽象化,只注重结果。

// 普通的add函数
function add(x, y) {
    return x + y
}
​
// Currying后
function curryingAdd(x) {
    return function (y) {
        return x + y
    }
}
​
add(1, 2)           // 3curryingAdd(1)(2)   // 3
// 可以变为--> 
const newFun = curryingAdd(x)
newFun(2)  // 3

16.事件循环?宏任务和微任务?

浏览器环境下,js是一种单线程语言,而它要做的事情非常多,如执行js代码、解析html为dom结构,解析css为dom添加样式以及各种的输入事件比如说网络请求、文件独写、用户交互事件等,为了不发生阻塞现象,引入了事件循环和消息队列的概念。

事件循环就是主线程中的任务循环执行,执行完毕不断取出消息队列中的任务来进行执行。

消息队列的任务包括js脚本、定时器、渲染事件、用户交互事件等等。

那为什么又要引入微任务呢?把任务又区分为宏任务和微任务呢?

因为执行js脚本的时候遇到了异步代码(promise的.then和定时器),怎么处理这种代码好呢?如果直接放到消息队列的尾部,因为消息队列是先进先出,所以前面任务太多的时候,异步代码要等很久,时效性很差。

于是引入微任务的概念,把微任务放到宏任务里面,主线程执行完当前宏任务之后,检查微任务队列,如果有微任务就执行。微任务队列为空的话当前宏任务出队列,主线程执行队列中的下一个宏任务。

宏任务包括:

js脚本、DOM操作、用户交互操作、网络请求、定时器等。

所以遇到定时器会被定义为下一个宏任务,执行优先级低于当前宏任务中的微任务。

宏任务:

  • script (可以理解为外层同步代码)
  • setTimeout/setInterval
  • UI rendering/UI事件
  • postMessage、MessageChannel
  • setImmediate、I/O(Node.js)

微任务:

  • Promise.then
  • MutaionObserver
  • Object.observe(已废弃;Proxy 对象替代)
  • process.nextTick(Node.js)

代码实例:

async function async1() {           
    console.log("async1 start");  //(2)        
    await  async2();            
    console.log("async1 end");   //(6)    
}        
async  function async2() {          
    console.log( 'async2');   //(3)     
}       
console.log("script start");  //(1)      
setTimeout(function () {            
    console.log("settimeout");  //(8)      
},0);        
async1();        
new Promise(function (resolve) {           
    console.log("promise1");   //(4)         
    resolve();        
}).then(function () {            
    console.log("promise2");    //(7)    
});        
console.log('script end');//(5)

先按顺序执行同步代码 从‘script start‘开始,

执行到setTimeout函数时,将其回调函数加入队列(此队列与promise队列不是同一个队列,执行的优先级低于promise。

然后调用async1()方法,await async2();//执行这一句后,输出async2后,await会让出当前线程,将后面的代码加到任务队列中,然后继续执行test()函数后面的同步代码

继续执行创建promise对象里面的代码属于同步代码,promise的异步性体现在then与catch处,所以promise1被输出,然后将then函数的代码加入队列,继续执行同步代码,输出script end。至此同步代码执行完毕。

开始从队列中调取任务执行,由于刚刚提到过,setTimeout的任务队列优先级低于promise队列,所以首先执行promise队列的第一个任务,因为在async函数中有await表达式,会使async函数暂停执行,等待表达式中的 Promise 解析完成后继续执行 async 函数并返回解决结果。

所以先执行then方法的部分,输出promise2,然后执行async1中await后面的代码,输出async1 end。。最后promise队列中任务执行完毕,再执行setTimeout的任务队列,输出settimeout。

setTimeout(fn,0)的含义是指某个任务在主线程最早可得的空闲时间执行。它在“任务队列”的尾部添加一个事件,因此要等到同步任务和“任务队列”现有的时间处理完才会得到执行。

总结:

读取整个代码,按照顺序将不同的任务推入主线程开始事件循环,推入的时候首先要区分出当前的宏任务,宏任务中若为同步代码,包括console.log()等正常执行的代码、紧挨在await后面的代码、 promise里面.then之前的代码,放到主线程直接执行;若为异步代码,如setTimeout和promise的then,则放到消息队列。

把消息队列区分为当前循环中的微任务和下次循环的宏任务。

当前循环宏任务的同步任务执行完毕,再从事件队列中拿到微任务的回调执行,如promise.then的代码,再执行下一次循环的宏任务的回调函数,如setTimeout里的代码。

17.设计模式?观察者模式?发布订阅模式?

设计模式是一种解决问题的思路的总结,由四人帮提出,大概分为:

创建型模式:工厂模式、原型模式等等

结构性模式:代理模式、过滤器模式

行为型模式:观察者模式、迭代器模式

观察者模式:

定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新。属于行为型模式,行为型模式注重对象之间通讯,而观察者模式注重观察者和被观察者之间通讯。

image-20220318093353693

观察者模式也叫发布订阅模式,但现在两者已经不同,前者没有中间的调度中心代理,后者已成为一种新的设计模式。

发布订阅模式:

当订阅目标即发布者发生改变时,通知到调度中心,再分别发送给订阅者。

18.浏览器渲染页面

css不会阻塞html得解析,而js代码会