前端面试基础题:Ajax请求发送、防抖和节流...
✨ 使用XMLHttpRequest发送Ajax请求
// ---------- GET 请求 ----------
// 创建xhr对象
var xhr = new XMLHttpRequest();
// 监听onreadystatechange事件
xhr.onreadystatechange = function () {
    if (xhr.readyState === 4 && xhr.status === 200) {
        console.log(xhr.responseText);
    }
}
// 调用open函数,指定请求方式与URL地址
// 第三个参数默认为true,表示异步发送请求
xhr.open('GET', 'url');
// 调用send函数,发起ajax请求
xhr.send();
// ---------- POST 请求 ----------
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
    if (xhr.readyState === 4 && xhr.status === 200) {
        console.log(xhr.responseText);
    }
}
xhr.open('POST', 'url');
// 设置Content-Type属性
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('key1=val1&key2=val2');
XMLHttpRequest对象的readyState表示当前Ajax所处的请求状态
| 值 | 状态 | 描述 | 
|---|---|---|
| 0 | UNSENT | xhr对象已创建,但尚未调用open函数 | 
| 1 | OPENED | open方法已调用 | 
| 2 | HEADERS_RECEIVED | send方法已调用,响应头也已被接收 | 
| 3 | LOADING | 数据接收中,response属性已经包含部分数据 | 
| 4 | DONE | ajax请求完成,数据传输已经彻底完成或失败 | 
XMLHttpRequest2的新特性
可以设置HTTP请求时限
xhr.timeout = 3000;
xhr.ontimeout = function () { console.log(‘请求超时’) }
可以使用FormData对象管理表单数据
var fd = new FormData(form);
xhr.send(fd);
可以上传文件 可以获得数据传输的进度信息
xhr.upload.onprogress = function (e) {
    // e.lengthComputable是一个布尔值,表示当前上传的资源是否局有可计算的长度
    if(e.lengthComputable) {
        // e.loaded已传输的字节
        // e.total需要传输的总字节
        // 当前进度除以总长度,乘以100以百分比输出,通过ceil向上取整
        var percentComplete = Math.ceil((e.loaded / e.total)*100);
        // 在控制台查看进度
        console.log(percentComplete+'%'); 
    }
}
✨ Fetch
- fetch是一个用于发送网络请求的API,它是基于Promise的。没有使用XMLHttpRequest对象。
- fetch采用模块化设计,API分散在多个对象上(Response对象、Request对象、Headers对象)。
- fetch只对网络请求报错,对400,500都当做成功的请求,服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject。
- fetch默认不会带cookie,需要添加配置项: fetch(url, {credentials: 'include'})
用法:
fetch('url')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(err => console.log("Oops, error", err))
fetch() 接受第二个可选参数,一个可以控制不同配置的 init 对象。
fetch('url', {
    method: 'GET',  // 请求方法('POST')
    mode: 'cors',  // 请求的模式('no-cors, same-origin')
    cache: 'no-cache',  //  请求的 cache 模式
    credentials: 'same-origin',  // 请求的 credentials,为了在当前域名内自动发送 cookie,必须提供这个选项
    header: {  // 请求头信息
        'Content-Type': 'application/json'
    },
    redirect: 'follow',  // 可用的 redirect 模式
    body: JSON.stringify(data),  // 请求的 body 信息
})
✨ Axios
专注于网络请求的库。
axios.get('url', { params: {} }).then(callback);
axios.post('url', {}).then(callback)
axios.ajax({
    methods: '请求类型',
    url: '请求URL地址',
    data: { // POST数据 },
    params: { // GET参数 }
}).then(callback)
Axios请求拦截器&响应拦截器:
- 请求拦截器:在请求发送前进行必要的操作处理,例如,添加统一cookie、请求体加验证、设置请求头等,相当于是对每个接口里相同操作的一个封装。
- 响应拦截器:在请求得到响应之后,对响应体的一些处理,通常是数据统一处理等,也常用来判断登录失效等。
import axios from 'axios'
// 创建axios实例
const instance = axios.create({
    // axios的一些配置
    baseURL: 'xxxxxxxx',
    timeout: 15000
});
// 配置请求拦截器
axios.interceptors.request.use(req => {
    // 发送请求前需要做的事情
    return req;
}, err => {
    // 在请求错误时要做的事情
    return Promise.reject(err);
});
// 配置响应拦截器
axios.interceptors.response.use(res => {
    // 收到响应后需要做的事情
    return res;
}, err => {
    // 请求错误时要做的事情
    return err;
})
✨ 同源策略和跨域
- 如果两个页面的协议,域名和端口都相同,则两个页面具有相同的源。反之,则为跨域
- 浏览器允许发起跨域请求,但是跨域请求回来的数据会被浏览器拦截,无法被页面获取到
- 如何实现跨域请求:JSONP和CORS
JSONP
- 通过 <script>脚本的src属性请求非同源的js脚本。只支持GET请求,不支持POST请求,它只支持跨域 HTTP 请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript 调用的问题
- 请求阶段:浏览器创建一个script标签,并给其src属性赋值,将回调函数的名称放到这个请求的query参数里(例如:http://example.com/api/?callback=jsonpCallback)
- 发送请求:当给script的src属性赋值时,浏览器就会发起一个请求
- 数据响应:服务端将要返回的数据作为参数和函数名称拼接在一起(jsonpCallback({name: 'abc'}))返回。服务端返回这个回调函数的执行,并将需要响应的数据放到回调函数的参数里,前端的script标签请求到这个执行的回调函数后会立马执行,于是就拿到了执行的响应数据。
<!-- 定义一个success回调函数 -->
<script>
    function success(data) {
        console.log('获取到了data数据');
        console.log(data);
    }
</script>
<!-- 通过<script>标签请求接口数据 -->
<script src="http://example.com/?callback=success&name=zs&age=18"></script>CORS
由一系列HTTP响应头组成,如果接口服务器配置了CORS相关的HTTP响应头,就可以解除浏览器端的跨域访问限制。不兼容某些低版本浏览器。CORS主要在服务器端进行配置。
- Access-Control-Allow-Origin:指定允许访问该资源的外域URL
- Access-Control-Allow-Headers:允许客户端向服务器发送额外的请求头信息
- Access-Control-Allow-Methods:允许客户端发起除GET、POST、HEAD之外的请求
CORS请求分类:
- 简单请求:同时满足以下两大条件的请求就属于简单请求:
- 请求方式:GET、POST、HEAD三者之一
- HTTP头部信息不超过以下几种字段:无自定义头部字段、Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width、Content-Type(只有三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain)
- 预检请求:除了简单请求都是复杂请求,在浏览器与服务器正式通信之前,浏览器会先发送OPTION请求进行预检,以获知服务器是否允许该实际请求,所以这一次的OPTIONS请求称为"预检请求",服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据。
// 在服务器端设置对应的http response header
function setCorsHeader (res) {
    res.setHeader('Access-Control-Allow-Origin', 'http://localhost:8082');
    res.setHeader('Access-Control-Allow-Credentials', true);  // 允许客户端携带验证信息
    res.setHeader('Access-Control-Allow-Headers', 'customer-header, customer-header2');
    res.setHeader('Access-Control-Allow-Methods', 'POST, PUT');
}
const server = http.createServer((req, res) => {
    const { method, url } = req;
    if (method === 'OPTIONS') {
        setCorsHeader(res);
    } else if (methods === 'PUT' && urlObj.pathname === '/cors') {
        setCorsHeader(res);
        res.serHeader('content-type', 'application/json');
        res.write(JSON.stringify(mockData));
    } else {
        res.statusCode = 404;
    }
    res.end();
})
代理服务器:跨域的问题根本原因就是返回数据的服务器和请求数据的页面不是一个源,那么就申请一个代理服务器,这个代理服务器和页面在同一个源,所以不会出现跨域的问题,那么这个代理服务器上没有我们需要的数据,所以就把这个请求再转发给有这个数据的服务器上,由于服务器和服务器之间通信不会出现跨域的问题,因为同源策略是浏览器上的,和服务器没关系,所以最后就可以成功把数据请求返回给浏览器。
只需要在配置文件中添加devServer: {proxy: 'http://localhost:5000'}配置,同时axios请求数据的地址指向代理服务器即可。
✨ 防抖和节流
- 防抖(多次触发,只执行最后一次):当事件被触发后,延迟n秒之后再执行回调,如果在这n秒内事件又被触发,则重新计时。应用场景:用户在输入框中连续输入一串字符时,可以通过防抖策略,只在输入完成后,才执行查询的请求,这样可以有效减少请求次数,节约请求资源。
- 节流(规定时间内,只触发一次):减少一段时间内事件的触发频率。应用场景:鼠标连续不断的触发某事件(如点击)只在单位时间触发一次。
// 防抖
function debounce(func, delay) {
    let timer = null;
    return function () {
        // 如果时间再次触发就清除定时器
        clearTimeout(timer);
        timer = setTimeout(function () {
            func.apply(this, arguments);
        }, delay);
    }
}
// 节流
function throttle(func, delay) {
    let flag = true;  // 通过闭包保存一个节流阀
    return function () {
        if (!flag) return;  // 如果有定时器在执行,则直接返回
        flag = false;
        setTimeout(function () {
            fn.apply(this, arguments);
            flag = true;
        }, delay);
    }
}
✨ 函数柯里化
柯里化(currying)又称部分求值。一个柯里化的函数首先会接收一些参数,接收了这些参数后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。
柯里化函数调用后,得到的是一个函数。柯里化可以帮助我们把相同的参数固定下来,把任意的多参函数通过固定参数的方式,变为单参函数,这样就不用每次调用函数的时候都去反复传递这些一样的参数了。
function curry(company, department) {
    return function (name, age) {
        console.log(`我是${company}${department}部门的${name}, ${age}岁`);
    }
}
let print = curry('xxx', 'yyy');  // 传递固定的公司、部门
print('zhangsan', 30);  // 调用 return 出来的函数并传递变化的参数
print('lisi', 20);
手写函数柯里化
const add = (a, b, c) => a + b + c;
const currying = (fn, ...args) => {
    let allArgs = [...args];
    const num = fn.length;
    const res = (...args2) => {
        allArgs = [...allArgs, ...args2];
        if (allArgs.length === num) {
            return fn(...allArgs);
        } else {
            return res;
        }
    }
    return res;
}
const a = currying(add, 1);
console.log(a(2)(3));
Node.js
✨ 模块化规范
- 模块:将一个复杂的程序依据一定的规则(规范)封装成几个块(文件),并组合在一起。块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信。
- 模块化好处:
- 避免命名冲突
- 更好的分离,按需加载
- 更高的复用性
- 高可维护性
 
- 引入多个<script>后出现的问题- 请求过多
- 依赖模糊
- 难以维护
 
CommonJS
- 主要应用于服务端开发,其加载方式是同步的。
- 导出模块:使用module.exports或exports来导出模块。
- 导入模块:使用require导入模块 。
// 定义模块 math.js
var add = function (x, y) { return x + y; };
exports.add = add;
// 导入模块 main.js
const math = require('./math');
console.log(math.add(2, 3));  // 5
AMD
- 主要应用于浏览器端开发,其加载方式是异步的。
- 导出模块:define函数中的return
- 导入模块:require
// 定义没有依赖的模块
define(function () {
    // 模块中所有代码全部放到这个函数中
    // return 模块
})
// 定义有依赖的模块
define(['module1', 'module2'], function(m1, m2) {
    // 模块中所有代码全部放到这个函数中
    // return 模块
})
// 引入模块
require(['module1', 'module2'], function(m1, m2) {
    // 模块加载成功之后的回调函数
    // 使用m1/m2
})
CMD
- 主要用于浏览器端开发,其加载方式是异步的。
- AMD强调模块加载的同时执行模块所需的代码,而CMD强调模块的按需加载,并且在使用时才执行模块中的代码。
- 导出模块:在define函数中使用exports或module.exports
- 导入模块:require
// 定义没有依赖的模块
define(function (require, exports, module) {
    exports.xxx = value
    module.exports.xxx = value
})
// 定义有依赖的模块
define(function (require, exports, module) {
    // 引入依赖模块(同步)
    var module2 = require('./module2')
    // 引入依赖模块(异步)
    require.async('./module3', function (m3) {})
    // 暴露模块
    exports.xxx = value
})
// 引入模块
define(function (require) {
    var m1 = require('./module1')
    var m2 = require('./module2')
    m1.show()
    m2.show()
})
CMD和AMD最大的区别是:AMD中要求依赖前置,也就是需要依赖哪个依赖就需要提前写好,然后加载完所有依赖之后才会继续执行接下来的代码。而CMD中依赖就近,延迟加载。不要求提前把所有的依赖全部写好,全部加载完再执行,而是可以就近书写,就是哪里用到了依赖只需要在哪里引入依赖即可,执行到这里的时候才会进行加载依赖操作。
ES6
- 官方的 JavaScript 模块化规范,用于客户端和服务端开发。
- 导出模块:export或export default
- 导入模块:import
// 定义模块
var num = 0;
var add = function (x, y) { return x + y; };
export { num, add }; 
// 导入模块
import { num, add } from './math.js'





