[前端基础] JavaScript 进阶篇

封装 Ajax

ajax 可以无需刷新页面与服务器进行通讯,允许根据用户事件来更新部分页面内容。

readyStatus 的值:

  • 0:未初始化
  • 1:启动;已经调用open()方法,但尚未调用send()方法。
  • 2:发送;已经调用send()方法,但尚未接收到相应。
  • 3:接收;已经收到部分响应数据。
  • 4:完成;已经收到全部响应数据,而且已经可以在客户端使用了

使用 promise 封装 ajax:

function transdata(data) {
    
    
    let arr = []
    for (let key in data) {
    
    
        arr.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key]))
    }
    return arr.join('&')
}

function Ajax(option) {
    
    
    return new Promise((resolve, reject) => {
    
    
        let xhr, timer
        let data = transdata(option.data)
        if (window.XMLHttpRequest) {
    
    
            xhr = new XMLHttpRequest()
        } else {
    
    
            // 兼容低版本ie浏览器
            xhr = new ActiveXObject("Microsoft.XMLHTTP")
        }
        if (option.type.toLowerCase() === "get") {
    
    
            // get 方法
            xhr.open(option.type, option.url + "?" + data, true)
            xhr.send()
        } else {
    
    
            // post 方法
            xhr.open(option.type, option.url, true)
            xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded")
            xhr.send(data)
        }

        xhr.onreadystatechange = function () {
    
    
            if (xhr.readyState === 4) {
    
    
                clearInterval(timer)
                if (xhr.status >= 200 && xhr.status <= 300 || xhr.status === 304) {
    
    
                    resolve(xhr.response)
                } else {
    
    
                    reject(new Error("error"))
                }
            }
        }
        // 超时处理
        if (option.timout) {
    
    
            timer = setInterval(function () {
    
    
                console.log("请求超时")
                xhr.abort()
                clearInterval(timer)
            }, option.timeout)
        }
    })
}

使用:

Ajax({
    
    
	type: "post",
	url: "/api/v1/lists",
	data: {
    
    
		"_gid": "1"
	},
	timeout: 3000
}).then(res => {
    
    
	cb(res)
})

Ajax 与 Fetch 区别

XMLHttpRequest 是一个设计粗糙的 API,不符合关注分离(Separation of Concerns)的原则,配置和调用方式非常混乱,而且基于事件的异步模型写起来也没有现代的 Promise,generator/yield,async/await 友好。

Fetch 基于标准 Promise 实现,支持 async/await,脱离了XHR,是ES规范里新的实现方式。

fetch(url).then(response => response.json()).then(data => console.log(data)).catch(e => console.log("Oops, error", e))

缺点:

  • Fetch 只对网络请求报错,对400,500都当做成功的请求,服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject。
  • Fetch 默认不会带 cookie,需要添加配置项: fetch(url, {credentials: 'include'})
  • Fetch 不支持 abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费。
  • Fetch 没有办法原生监测请求的进度,而XHR可以。

函数柯里化

函数柯里化指的是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。

function curry(fn, args) {
    
    
	// 获取函数需要的参数长度
	let length = fn.length;
	let args = args || [];
	return function() {
    
    
		let newArgs = args.concat(Array.prototype.slice.call(arguments));
		// 判断参数的长度是否已经满足函数所需参数的长度
		if (newArgs.length >= length) {
    
    
			// 如果满足,执行函数
			return fn.apply(this, newArgs);
		} else {
    
    
			// 如果不满足,递归返回科里化的函数,等待参数的传入
			return curry.call(this, fn, newArgs);
		}
	}
};
// es6 实现 
function curry(fn, ...args) {
    
    
	return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}

应用:参数复用

function url(protocol, hostname, pathname) {
    
    
	return `${
      
      protocol}${
      
      hostname}${
      
      pathname}`;
}

let curry_uri = curry(url)
const base_uri = curry_uri('https://', 'www.github.com')
const uri1 = base_uri('/a/') //'https://www.github.com/a/'
const uri2 = base_uri('/b/') //'https://www.github.com/b/'
const uri3 = base_uri('/c/') //'https://www.github.com/c/'

经典面试题:实现 add 方法

add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;

下面这个只能两个数据相加

function add(a, b) {
    
    
	return a + b
}

let curry_add = curry(add)
curry_add(1)(2) // 3;

改进后全部参数可以相加

function add() {
    
    
  // 将传入的不定参数转为数组对象
  let args = Array.prototype.slice.call(arguments);

  // 递归:内部函数里面进行自己调用自己
  // 当 add 函数不断调用时,把第 N+1 个括号的参数加入到第 N 个括号的参数里面
  let inner = function() {
    
    
    args.push(...arguments);
    return inner;
  }
  
  inner.toString = function() {
    
    
    // args 里的值不断累加
    return args.reduce(function(prev, cur) {
    
    
      return prev + cur;  
    });
  };

  return inner;
}

add(1)(2)(3,4).toString() // 10
add(1)(2)(3)(4).toString() // 10

防抖节流

防抖

原理:在事件触发 n 秒后才执行,如果在一个事件触发的 n 秒内又触发了这个事件,那就以新的事件的时间为准,n 秒后才执行。

简易版防抖实现:

function debounce(fn, delay = 1000) {
    
    
    let time = null
    function _debounce() {
    
    
	    var context = this
	    var args = arguments
        if (time !== null) {
    
    
            clearTimeout(time)
        }
        time = setTimeout(() => {
    
    func.apply(context, args)}, delay)
    }
    return _debounce
}

不希望非要等到事件停止触发后才执行,希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。

function debounce(func, wait, immediate) {
    
    
    var timeout, result
    var debounced = function () {
    
    
        var context = this
        var args = arguments

        if (timeout) clearTimeout(timeout)
        if (immediate) {
    
    
            // 如果已经执行过,不再执行
            var callNow = !timeout
            timeout = setTimeout(function(){
    
    
                timeout = null
            }, wait)
            if (callNow) result = func.apply(context, args)
        }
        else {
    
    
            timeout = setTimeout(function(){
    
    
                func.apply(context, args)
            }, wait)
        }
        return result
    }

	// 取消防抖
    debounced.cancel = function() {
    
    
        clearTimeout(timeout)
        timeout = null
    };

    return debounced
}

//使用
window.onresize = debounce(XXXfunction, 10000, true);

节流

持续触发事件,每隔一段时间,只执行一次事件。可分为首次是否执行以及结束后是否执行。

简易版节流实现:

function throttle(fn, interval) {
    
    
    let lastTime = 0
    const _throttle = function(...args) {
    
    
        const nowTime = new Date().getTime()
        const remainTime = nowTime - lastTime
        if (remainTime - interval >= 0) {
    
    
            fn.apply(this, args)
            lastTime = nowTime
        }
    }
    return _throttle
}

options参数 :

  • leading:false 表示禁用第一次执行
  • trailing: false 表示禁用停止触发的回调
function throttle(func, wait, options) {
    
    
    var timeout, context, args, result;
    var previous = 0;
    if (!options) options = {
    
    };

    var later = function() {
    
    
        previous = options.leading === false ? 0 : new Date().getTime();
        timeout = null;
        func.apply(context, args);
        if (!timeout) context = args = null;
    };

    var throttled = function() {
    
    
        var now = new Date().getTime();
        if (!previous && options.leading === false) previous = now;
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
        if (remaining <= 0 || remaining > wait) {
    
    
            if (timeout) {
    
    
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            func.apply(context, args);
            if (!timeout) context = args = null;
        } else if (!timeout && options.trailing !== false) {
    
    
            timeout = setTimeout(later, remaining);
        }
    };
	throttled.cancel = function() {
    
    
	    clearTimeout(timeout);
	    previous = 0;
	    timeout = null;
	}
    return throttled;
}

深拷贝

存在各种问题的方法:

  • let b = JSON.parse(JSON.stringify(a))
  • let b= [].concat(a)
  • let b = a.slice(),只有一级属性值的数组对象
  • Object.assign(),当对象中只有一级属性,没有二级属性的时候,此方法为深拷贝,但是对象中有对象的时候,此方法,在二级属性以后就是浅拷贝
  • let b = [...a]

递归拷贝

const deepClone = (tar, map = new WeakMap) => {
    
    
  if (!(typeof tar === 'object' && tar != null)) return tar
  if (map.has(tar)) return map.get(tar)

  if (tar instanceof Map || tar instanceof Set) {
    
    
    const retObj = new tar.constructor()
    map.set(tar, retObj) // 在调用 deepClone 之前将 tar-retObj 记录到 map 中, 防止循环引用

    retObj.add ? 
      tar.forEach(val => retObj.add(deepClone(val, map))) : 
      tar.forEach((val, key) => retObj.set(key, deepClone(val, map)))

  } else if (tar instanceof Date || tar instanceof RegExp) {
    
    
    const retObj = new tar.constructor(tar)
    map.set(tar, retObj)
  } else {
    
     // 对象或数组
    const retObj = Object.create(Reflect.getPrototypeOf(tar), Object.getOwnPropertyDescriptors(tar))
    map.set(tar, retObj)
    Reflect.ownKeys(tar).forEach(key => {
    
    
      retObj[key] = deepClone(tar[key], map)
    })
  }

  return retObj
}

数组去重

  1. 双重 for 循环。NaN 和 {} 没有去重,因为 NaN != NaN,null 直接消失。
function unique(arr){
    
                
        for(var i=0; i<arr.length; i++){
    
    
            for(var j=i+1; j<arr.length; j++){
    
    
                if(arr[i]==arr[j]){
    
    
                    arr.splice(j,1);
                    j--;
                }
            }
        }
return arr;
}
  1. indexOf 方法。NaN、{} 没有去重,因为 [NaN].indexOf(NaN) // -1
function unique(arr) {
    
    
    if (!Array.isArray(arr)) {
    
    
        console.log('type error!')
        return
    }
    var array = [];
    for (var i = 0; i < arr.length; i++) {
    
    
        if (array.indexOf(arr[i]) === -1) {
    
    
            array.push(arr[i])
        }
    }
    return array;
}
  1. includes 方法。空 {} 未去重。
function unique(arr) {
    
    
    if (!Array.isArray(arr)) {
    
    
        console.log('type error!')
        return
    }
    var array =[];
    for(var i = 0; i < arr.length; i++) {
    
    
            if( !array.includes( arr[i]) ) {
    
    
                    array.push(arr[i]);
              }
    }
    return array
}

function unique(arr){
    
    
    return arr.reduce((prev,cur) => prev.includes(cur) ? prev : [...prev,cur],[]);
}
  1. hasOwnProperty 方法。能全部去重。
function unique(arr) {
    
    
    var obj = {
    
    };
    return arr.filter(function(item, index, arr){
    
    
        return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
    })
}
  1. filter 方法。
function unique(arr) {
    
    
  return arr.filter(function(item, index, arr) {
    
    
    //当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
    return arr.indexOf(item, 0) === index;
  });
}
  1. set 方法。也无法去重空 {} 对象。
function unique (arr) {
    
    
  return Array.from(new Set(arr))
}

function unique (arr) {
    
    
  return [...new Set(arr)] 
}
  1. map 方法。
function arrayNonRepeatfy(arr) {
    
    
  let map = new Map();
  let array = new Array();  // 数组用于返回结果
  for (let i = 0; i < arr.length; i++) {
    
    
    if(map.has(arr[i])) {
    
      // 如果有该key值
      map.set(arr[i], true); 
    } else {
    
     
      map.set(arr[i], false);   // 如果没有该key值
      array.push(arr[i]);
    }
  } 
  return array ;
}

手写 call、apply、bind

call

Function.prototype.myCall = function() {
    
    
	const [context, ...args] = [...arguments]
    context.fn = this
    const res = context.fn(args)
    delete context.fn
    return res
}

let obj = {
    
    
    value: 1
}
function bar(name, age) {
    
    
    console.log(this.value);
    return {
    
    
        value: this.value,
        name: name,
        age: age
    }
}
console.log(bar.myCall(obj, 'kevin', 18));
// 1
// Object {
    
    
//    value: 1,
//    name: 'kevin',
//    age: 18
// }

apply

Function.prototype.myApply = function(context, arr) {
    
    
    context.fn = this
    let res = !args ? context.fn() : context.fn(...args)
    delete context.fn
    return res
}
console.log(bar.apply(obj, ['kevin', 18]))
// 1
// Object {
    
    
//    value: 1,
//    name: 'kevin',
//    age: 18
// }

bind

Function.prototype.myBind = function(context) {
    
    
    let self = this
    let args = Array.prototype.slice.call(arguments, 1)
    let fNOP = function () {
    
    }
    let fBound = function () {
    
    
        let bindArgs = Array.prototype.slice.call(arguments)
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs))
    }
    fNOP.prototype = this.prototype
    fBound.prototype = new fNOP()
    return fBound
}

手写 new

new 一个函数时:

  1. 创建一个新对象
  2. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
  3. 执行构造函数中的代码(为这个新对象添加属性)
  4. 返回新对象
function myNew() {
    
    
	let constr = Array.prototype.shift.call(arguments)
	// 1. 创建一个新对象
	let obj = Object.create(constr.prototype)
	// 2. 将构造函数的作用域赋给新对象,并执行构造函数中的代码
	let result = constr.apply(obj, arguments)
	// 如果构造函数有返回值,则返回;否则,就会默认返回新对象。
	return result instanceof Object? result : obj
}

手写 promise

MDN Promise

一个 Promise 对象代表一个在这个 promise 被创建出来时不一定已知值的代理。它让你能够把异步操作最终的成功返回值或者失败原因和相应的处理程序关联起来。这样使得异步方法可以像同步方法那样返回值:异步方法并不会立即返回最终的值,而是会返回一个 promise,以便在未来某个时候把值交给使用者。

Promise 包含3种状态:

  • 待定(pending):初始状态,既没有被兑现,也没有被拒绝。
  • 已兑现(fulfilled):意味着操作成功完成。
  • 已拒绝(rejected):意味着操作失败。

在这里插入图片描述
基础实例:

let myFirstPromise = new Promise((resolve, reject) => {
    
    
  // We call resolve(...) when what we were doing asynchronously was successful, and reject(...) when it failed.
  // In this example, we use setTimeout(...) to simulate async code.
  // In reality, you will probably be using something like XHR or an HTML5 API.
  setTimeout( function() {
    
    
    resolve("Success!")  // Yay! Everything went well!
  }, 250)
})

myFirstPromise.then((successMessage) => {
    
    
  // successMessage is whatever we passed in the resolve(...) function above.
  // It doesn't have to be a string, but if it is only a succeed message, it probably will be.
  console.log("Yay! " + successMessage)
}).catch(err => {
    
     console.log(err) });

Promise 的实现

class MyPromise {
    
    
  constructor(executor) {
    
    
    this.state = 'pending'
    this.value = null
    this.reason = null
    this.callbacks = []
    const resolve = value => {
    
    
      if (this.state !== 'pending') return
      this.state = 'fulfilled'
      this.value = value
      this.callbacks.forEach(callback => callback.fulfilled(value))
    }
    const reject = reason => {
    
    
      if (this.state !== 'pending') return
      this.state = 'rejected'
      this.reason = reason
      this.callbacks.forEach(callback => callback.rejected(reason))
    }
    try {
    
    
      executor(resolve, reject)
    } catch (error) {
    
    
      reject(error)
    }
  }
  then(onFulfilled, onRejected) {
    
    
    if(typeof onFulfilled !== 'function') onFulfilled = value => value
    if(typeof onRejected !== 'function') onRejected = reason => {
    
     throw reason }
    let promise = new MyPromise((resolve, reject) => {
    
    
      if (this.state === 'fulfilled') {
    
    
        setTimeout(() => {
    
    
          try {
    
    
            this.resolvePromise(promise, onFulfilled(this.value), resolve, reject)
          } catch (error) {
    
    
            reject(error)
          }
        });
      }
      if (this.state === 'rejected') {
    
    
        setTimeout(() => {
    
    
          try {
    
    
            this.resolvePromise(promise, onRejected(this.reason), resolve, reject)
          } catch (error) {
    
    
            reject(error)
          }
        })
      }
      if (this.state === 'pending') {
    
    
        this.callbacks.push({
    
    
          fulfilled: () => {
    
    
            setTimeout(() => {
    
    
              try {
    
    
                this.resolvePromise(promise, onFulfilled(this.value), resolve, reject)
              } catch (error) {
    
    
                reject(error)
              }
            })
          },
          rejected: () => {
    
    
            setTimeout(() => {
    
    
              try {
    
    
                this.resolvePromise(promise, onRejected(this.reason), resolve, reject)
              } catch (error) {
    
    
                reject(error)
              }
            })
          }
        })
      }
    })
    return promise
  }
  resolvePromise(promise, result, resolve, reject) {
    
    
    if (promise === result) reject(new TypeError('Chaining cycle detected for promise'))
    if (result && typeof result === 'object' || typeof result === 'function') {
    
    
      let called
      try {
    
    
        let then = result.then
        if (typeof then === 'function') {
    
    
          then.call(result, value => {
    
    
            if (called) return
            called = true
            this.resolvePromise(promise, value, resolve, reject)
          }, reason => {
    
    
            if (called) return
            called = true
            reject(reason)
          })
        } else {
    
    
          if (called) return
          called = true
          resolve(result)
        }
      } catch (error) {
    
    
        if (called) return
        called = true
        reject(error)
      }
    } else {
    
    
      resolve(result)
    }
  }
}

手写 jsonp

ajax 请求受同源策略影响,不允许进行跨域请求,而 script 标签 src 属性中的链接却可以访问跨域的 js 脚本,利用这个特性,服务端不再返回 JSON 格式的数据,而是返回一段调用某个函数的 js 代码,在 src 中进行了调用,这样实现了跨域。

因为 jsonp 发送的并不是 ajax 请求,而是动态创建script标签。script 标签是没有同源限制的,把 script 标签的 src 指向请求的服务端地址。

缺点:

  • JSON 只支持 get,因为 script 标签只能使用 get 请求
  • JSONP 需要后端配合返回指定格式的数据
//手写jsonp,函数中传入参数,url,params,callbackName
function jsonp({
     
      url, params, callbackName }) {
    
    
    //定义一个函数拼接url字符串
    function getUrl() {
    
    
        let dataSrc = ''
        for (let key in params) {
    
    
            if(params.hasOwnProperty(key)) {
    
    
                dataSrc += `${
      
      key}=${
      
      params[key]}&`
            }
        }
        dataSrc+=`callBack=${
      
      callbackName}`
        return `${
      
      url}?${
      
      dataSrc}`
    }
    return new Promise((resolve,reject)=>{
    
    
        var scriptEle = document.createElement('script')
        scriptEle.src = getUrl
        document.body.appendChild(scriptEle)
        window[callbackName] = data => {
    
    
            resolve(data)
            document.removeChild(scriptEle)
        }
    })
}

async/await

async 函数是使用async关键字声明的函数。async 函数是 AsyncFunction 构造函数的实例,并且其中允许使用 await 关键字。async 和 await 关键字让我们可以用一种更简洁的方式写出基于 Promise 的异步行为,而无需刻意地链式调用 promise。本质是 Promise 的语法糖,用于优化 then 链式调用。

async 函数一定会返回一个 promise 对象。如果一个 async 函数的返回值看起来不是 promise,那么它将会被隐式地包装在一个 promise 中:

async function foo() {
    
    
  await 1;
}
// 等价于如下写法
function foo() {
    
    
  return Promise.resolve(1);
}

async 函数的函数体可以被看作是由 0 个或者多个 await 表达式分割开来的。从第一行代码直到(并包括)第一个 await 表达式(如果有的话)都是同步运行的。这样的话,一个不含 await 表达式的 async 函数是会同步运行的。然而,如果函数体内有一个 await 表达式,async 函数就一定会异步执行。

async function foo() {
    
    
  await 1;
}
// 等价于如下写法
function foo() {
    
    
  return Promise.resolve(1).then(() => undefined);
}

await 必须在 async 函数中使用:

async function() {
    
    
	const user_info = await getUserInfo(params)
}

Event Loop

参考:https://juejin.cn/post/6844903764202094606?utm_source=gold_browser_extension#heading-0

视频:https://www.bilibili.com/video/BV1Si4y1c7rh/?spm_id_from=333.1007.top_right_bar_window_history.content.click

实现一个简单的 Node 的 Events 模块

Node 中的 events 模块:

const EventEmitter = require('events')
const ev = new EventEmitter()
// on 
ev.on('事件1', () => {
    
    
  console.log('事件1执行了')
})
// emit触发
ev.emit('事件1')

// once 首次触发
ev.once('事件1', () => {
    
    
  console.log('事件1执行了')
})
// 移除
ev.off('事件1', callBackFn)

Events 模块本质上是观察者模式的实现,所谓观察者模式就是:它定义了对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知。观察者模式有对应的观察者以及被观察的对象,在 Events 模块中,对应的实现就是 on 和 emit 函数。

class EventEmitter{
    
    
	constructor(){
    
    
		// 事件监听函数保存的地方
		this.events={
    
    };
	}
	on(eventName,listener){
    
    
		if (this.events[eventName]) {
    
    
			this.events[eventName].push(listener);
		} else {
    
    
			// 如果没有保存过,将回调函数保存为数组
			this.events[eventName] = [listener];
		}
	}
	emit(eventName,...rest){
    
    
		// emit触发事件,把回调函数拉出来执行
		this.events[eventName] && this.events[eventName].forEach(listener => listener.apply(this,rest))
	}
}

猜你喜欢

转载自blog.csdn.net/by6671715/article/details/128007960