前端手写篇

Posted quitv

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端手写篇相关的知识,希望对你有一定的参考价值。

Object.getPrototypeOf是一个javascript内置函数,用于获取指定对象的原型

Object.create() 是 JavaScript 中用来创建一个新对象,并且可以将其设置为继承自另一个对象的原型对象

1.实现防抖函数(debounce)

防抖函数原理:把触发非常频繁的事件合并成一次去执行 在指定时间内只执行一次回调函数,如果在指定的时间内又触发了该事件,则回调函数的执行时间会基于此刻重新开始计算。

eg. 像百度搜索,就应该用防抖,当我连续不断输入时,不会发送请求;当我一段时间内不输入了,才会发送一次请求;如果小于这段时间继续输入的话,时间会重新计算,也不会发送请求。

手写简化版:

// func是用户传入需要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => 
  // 缓存一个定时器id
  let timer = 0
  // 这里返回的函数是每次用户实际调用的防抖函数
  // 如果已经设定过定时器了就清空上一次的定时器
  // 开始一个新的定时器,延迟执行用户传入的方法
  return function(...args) 
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => 
      func.apply(this, args)
    , wait)
  


 

 

适用场景:

  • 文本输入的验证,连续输入文字后发送 AJAX 请求进行验证,验证一次就好。
  • 按钮提交场景:防止多次提交按钮,只执行最后提交的一次。
  • 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似。

2.实现节流函数(throttle)

节流函数原理:指频繁触发事件时,只会在指定的时间段内执行事件回调,即触发事件间隔大于等于指定的时间才会执行回调函数。总结起来就是:事件,按照一段时间的间隔来进行触发

像dom的拖拽,如果用防抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多

手写简版

使用时间戳的节流函数会在第一次触发事件时立即执行,以后每过 wait 秒之后才执行一次,并且最后一次触发事件不会被执行。

时间戳方式:

    const throttle = (func, wait = 50) => 
            let lastTime = 0;
            return function (...args) 
                let now = new Date();
                if (now - lastTime > wait) 
               
                    lastTime = now;
                    func.apply(this,args)
                
            
        
        setInterval(
            throttle(()=>
                console.log('1')
            ,1000)
        ,1)

定时器方式:

使用定时器的节流函数在第一次触发时不会执行,而是在 delay 秒之后才执行,当最后一次停止触发后,还会再执行一次函数

  const throttle1 =(func, wait = 50) => 
            let timer = null ;
            return function(...args)
                if(!timer)
                    timer = setTimeout(()=>
                        func.apply(this,args);
                        timer = null 
                    ,wait)
                
            
        

        setInterval(
            throttle1(()=>
                console.log('1')
            ,1000)
        ,1)

适用场景:

  • DOM 元素的拖拽功能实现(mousemove
  • 搜索联想(keyup
  • 计算鼠标移动的距离(mousemove
  • Canvas 模拟画板功能(mousemove
  • 监听滚动事件判断是否到页面底部自动加载更多
  • 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
  • 缩放场景:监控浏览器resize
  • 动画场景:避免短时间内多次触发动画引起性能问题

总结

  • 函数防抖:将几次操作合并为一次操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。
  • 函数节流:使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。

3 实现instanceOf

  • 步骤1:先取得当前类的原型,当前实例对象的原型链
  • ​步骤2:一直循环(执行原型链的查找机制)
    • 取得当前实例对象原型链的原型链(proto = proto.__proto__,沿着原型链一直向上查找)。
    • 如果 当前实例的原型链__proto__上找到了当前类的原型prototype,则返回 true。
    • 如果 一直找到Object.prototype.__proto__ == nullObject的基类(null)上面都没找到,则返回 false。
 // 实例.__proto__ = 类.prototype
        function _instanceof(example, classFunc) 
            // 基本数据类型直接返回false
            if (typeof example !== 'object' || example === null) return false;
            let proto = Object.getPrototypeOf(example);
            while (true) 
                if (proto == null) return false;
                if (proto == classFunc.prototype); return true
                proto = Object.getPrototypeOf(proto)
            
        
        console.log('test', _instanceof(null, Array)) // false
        console.log('test', _instanceof([], Array)) // true
        console.log('test', _instanceof('', Array)) // false
        console.log('test', _instanceof(, Object)) // true

4.实现new的过程

new操作符做了这些事:

  • 创建一个全新的对象
  • 这个对象的__proto__要指向构造函数的原型prototype
  • 执行构造函数,使用 call/apply 改变 this 的指向
  • 返回值为object类型则作为new方法的返回值返回,否则返回上述全新对象
function myNew(fn, ...args) 
  // 基于原型链 创建一个新对象
  let newObj = Object.create(fn.prototype);
  // 添加属性到新对象上 并获取obj函数的结果
  let res = fn.apply(newObj, args); // 改变this指向

  // 如果执行结果有返回值并且是一个对象, 返回执行的结果, 否则, 返回新创建的对象
  return typeof res === 'object' ? res: newObj;


// 用法
function Person(name, age) 
  this.name = name;
  this.age = age;

Person.prototype.say = function() 
  console.log(this.age);
;
let p1 = myNew(Person, "poety", 18);
console.log(p1.name);
console.log(p1);
p1.say();


 

 

5.实现call方法

call做了什么:

  • 将函数设为对象的属性
  • 执行和删除这个函数
  • 指定this到函数并传入给定参数执行函数
  • 如果不传入参数,默认指向为 window
Function.prototype.myCall = function (context = window, ...args) 
            if (typeof this !== 'function') 
                throw new Error('type error')
            
            let key = Symbol('key');
            // this就是fn
            // this() fn()
            context[key] = this;
            // 利用context进行调用
            let result = context[key](...args);
            delete context[key];
            return result
        

        //用法:f.call(obj,arg1)
        function f(a, b) 
            console.log(a + b)
            console.log(this.name)
        
        let obj = 
            name: 1
        
        f.myCall(obj, 1, 2) //否则this指向window

6.实现apply方法

思路: 利用this的上下文特性。apply其实就是改一下参数的问题

Function.prototype.myApply = function (context = window, args) 
            if (typeof this !== 'function') 
                throw new Error('type Error')
            
            let key = Symbol('key')
            context[key] = this;
            let result = context[key](...args)
            delete context[key]
            return result
        
        // 使用
        function f(a, b) 
            console.log(a, b)
            console.log(this.name)
        
        let obj = 
            name: '张三'
        
        f.myApply(obj, [1, 2]) //arguments[1]

7.实现bind方法

bind 的实现对比其他两个函数略微地复杂了一点,涉及到参数合并(类似函数柯里化),因为 bind 需要返回一个函数,需要判断一些边界问题,以下是 bind 的实现

Function.prototype.myBind = function (context, ...args1) 
  let fn = this;
  return function (...args2) 
    return fn.apply(context, [...args1, ...args2]);
  ;
;

8.实现深拷贝

简单版:

const newObj = JSON.parse(JSON.stringify(oldObj));

局限性:

  • 他无法实现对函数 、RegExp等特殊对象的克隆,因为在JSON中,函数和RegExp等特殊对象的值会被转化为 null
  • 会抛弃对象的constructor,所有的构造函数会指向Object,由于 JSON.stringify() 会把对象转化为 JSON 字符串,再通过 JSON.parse() 解析成对象,这个过程中会丢失对象的 constructor 属性,导致所有对象的 constructor 都指向 Object。
  • 对象有循环引用,会报错,当被克隆的对象中存在循环引用时,JSON.stringify() 会抛出异常,因为 JSON 格式不支持循环引用。例如,如果对象A中引用了对象B,而对象B又引用了对象A,那么 JSON.stringify(A) 会抛出异常。

面试简版

function deepClone(obj) 
    // 如果是 值类型 或 null,则直接return
    if(typeof obj !== 'object' || obj === null) 
      return obj
    
    
    // 定义结果对象
    let copy = 
    
    // 如果对象是数组,则定义结果数组
    if(obj.constructor === Array) 
      copy = []
    
    
    // 遍历对象的key
    for(let key in obj) 
        // 如果key是对象的自有属性
        if(obj.hasOwnProperty(key)) 
          // 递归调用深拷贝方法
          copy[key] = deepClone(obj[key])
        
    
    
    return copy
 
调用深拷贝方法,若属性为值类型,则直接返回;若属性为引用类型,则递归遍历。这就是我们在解这一类题时的核心的方法。

进阶版

  • 解决拷贝循环引用问题
  • 解决拷贝对应原型问题
function deepClone(value, hash = new WeakMap) 
            // 如果是值类型或者null ,则直接return
            if (value == null) return value
            if (value instanceof RegExp) return new RegExp(value);
            if (value instanceof Date) return new Date(value);
            if (typeof value !== 'object') 
                return value
            
            let obj = new value.constructor();
            // 说明是一个对象类型 
            if (hash.get(value)) 
                return hash.get(value)
            
            hash.set(value, obj);
            // 遍历对象的key
            for (let key in value) 
                // 如果key是对象自有属性
                if (value.hasOwnProperty(key)) 
                    obj[key] = deepClone(value[key], hash)
                
            
            return obj
        
        var o = ;
        o.x = o;
        var o1 = deepClone(o); // 如果这个对象拷贝过了 就返回那个拷贝的结果就可以了
        console.log(o1);

9.实现类的继承

1. 寄生组合继承

  function Parent(name) 
            this.name = name;
        
        Parent.prototype.say = function () 
            console.log(this.name + ` say`);
        
        Parent.prototype.play = function () 
            console.log(this.name + ` play`);
        

        function Child(name, parent) 
            // 
            Parent.call(this, parent);
            this.name = name;
        
        Child.prototype = Object.create(Parent.prototype)
        Child.prototype.say = Parent.prototype.play = function () 
            console.log(this.name + ` play`);
        
        // 注意记得把子类的构造指向子类本身
        Child.prototype.constructor = Child;

        var parent = new Parent('parent');
        parent.say()

        var child = new Child('child');
        child.say()
        child.play(); // 继承父类的方法

10.实现Promise相关方法

1.实现Promise相关方法

实现 resolve 静态方法有三个要点:

  • 传参为一个 Promise, 则直接返回它。
const promise1 = Promise.resolve('resolved');

const resultPromise = Promise.resolve(promise1);

console.log(resultPromise === promise1); // true

在这个例子中,promise1 是一个已经 resolved 的 Promise 对象,调用 Promise.resolve 方法时传入了 promise1 对象,因此返回的 Promise 对象直接就是 promise1 对象本身,而不是新创建的一个 Promise 对象。因此,resultPromisepromise1 引用同一个对象,它们完全相等

  • 传参为一个 thenable 对象,返回的 Promise 会跟随这个对象,采用它的最终状态作为自己的状态。
const thenable = 
  then(resolve, reject) 
    setTimeout(() => 
      resolve('resolved from thenable');
    , 1000);
  
;

const resultPromise = Promise.resolve(thenable);

resultPromise.then(value => 
  console.log(value); // resolved from thenable
);

在这个例子中,thenable 对象具有 then 方法,因此可以认为它是一个 thenable 对象。调用 Promise.resolve 方法时传入了 thenable 对象,因此返回的 Promise 对象会跟随 thenable 对象,等待它的状态发生变化。在 thenable 对象的 then 方法中,我们使用了 setTimeout 来模拟异步操作,1 秒后将其状态设置为 resolved,这时候返回的 Promise 对象的状态也会相应地变为 resolved,成功回调函数会被调用,并输出 resolved from thenable

  • 其他情况,直接返回以该值为成功状态的promise对象。
const resultPromise = Promise.resolve('resolved');

resultPromise.then(value => 
  console.log(value); // resolved
);

调用 Promise.resolve 方法时传入了一个普通的字符串 'resolved',因此返回的 Promise 对象的状态会直接被设置为 resolved,成功回调函数会被调用,并输出 'resolved'

Promise.reject = (param) => 
            // 如果 param 是一个 Promise 对象,则直接返回该对象,不需要再创建新的 Promise 对象。
            if (param instanceof Promise) return param;
            // 如果 param 是一个 thenable 对象,则返回一个 Promise 对象,该对象的状态会跟随 param 对象,采用它的最终状态作为自己的状态。
            if(param && param.then && typeof param.then==='function')
                // param 状态变为成功会调用resolve,将新 Promise 的状态变为成功,反之亦然
                param.then(resolve,reject)
            else
                // 如果 param 不是 Promise 对象也不是 thenable 对象,则返回一个以 param 为成功状态的 Promise 对象。 
                resolve(param)
            
        

2 实现 Promise.reject

Promise.reject 中传入的参数会作为一个 reason 原封不动地往下传, 实现如下:

Promise.reject = function (reason) 
    return new Promise((resolve, reject) => 
        reject(reason);
    );

3 实现 Promise.prototype.finally

  1. 无论 Promise 的状态是成功还是失败,finally 方法中的回调函数都会被执行。
  2. 如果 finally 回调函数中返回的 Promise 对象被 reject,且前面没有捕获该错误的处理函数,那么该错误会传递到后面的 thenerr 处理函数中。

    如果 finally 回调函数中返回的 Promise 对象被 resolve,那么它将不会影响原来 Promise 对象的状态和值,也不会改变后面的 then 的状态和值。

  3. 如果 finally 方法中有异步操作,如 Promise,它会等待异步操作完成后再将原 Promise 对象的状态传递给下一个 then 方法。也就是说,如果 finally 方法中包含异步操作,后面的 then 方法会等待它们全部执行完毕,再将原 Promise 对象的状态传递给下一个 then 方法。
  Promise.prototype.finally = function (callback) 
            //返回一个新的promise对象
            return this.then((data) => 
                return Promise.resolve(callback()).then(() => data)
            , err => 
                return Promise.resolve(callback()).then(() => 
                    throw err
                )
            )
        

4 实现 Promise.all

1.如果传入参数为一个空的可迭代对象,则直接进行 resolve。

如果传入的可迭代对象是空的,即没有任何元素,Promise.all 方法会立即返回一个已完成的 Promise 对象,其值为一个空数组。例如:

Promise.all([]).then((result) => 
  console.log(result); // []
);

2.如果参数中有一个 promise 失败,那么 Promise.all 返回的 promise 对象失败。

Promise.all([
  Promise.resolve(1),
  Promise.reject(new Error("Error")),
  Promise.resolve(3)
]).catch((error) => 
  console.log(error); // Error: Error
);

3.在任何情况下,Promise.all 返回的 promise 的完成状态的结果都是一个数组。

Promise.all([
  Promise.resolve(1),
  Promise.resolve(2),
  Promise.resolve(3)
]).then((result) => 
  console.log(result); // [1, 2, 3]
);
Promise.all = function (promises) 
            return new Promise((resolve, reject) => 
                // 存储每一个 promise 对象的解决结果
                let result = [];
                // 计数器 index 来记录已解决的 promise 数量。
                let index = 0;
                let len = promises.length;
                if (len === 0) 
                    resolve(result)
                    return;
                
                for (let i = 0; i < len; i++) 
                    Promise.resolve(promises[i]).then(data => 
                        result[i] = data;
                        index++
                        if (index === len) resolve(result)
                    ).catch(err => 
                        reject(err)
                    )
                

            )
        

5 实现promise.allsettle

Promise.allSettled()方法返回一个在所有给定的promise都已经fulfilled或rejected后的promise,并带有一个对象数组,每个对象表示对应的promise`结果

假设有三个异步函数 fetchData1(), fetchData2(), fetchData3() 分别用来从服务器获取数据,这些函数都返回一个 Promise 对象。我们想要同时获取这三个异步操作的结果,并根据结果采取不同的操作,我们可以使用 Promise.allSettled() 方法来实现。

const promises = [fetchData1(), fetchData2(), fetchData3()];

Promise.allSettled(promises)
  .then(results => 
    results.forEach(result => 
      if (result.status === 'fulfilled') 
        console.log(`获取数据成功: $result.value`);
       else 
        console.log(`获取数据失败: $result.reason`);
      
    );
  );

在这个例子中,我们创建了一个 Promise 对象数组 promises,其中包含了三个异步函数的返回值。然后,我们调用 Promise.allSettled(promises) 方法来等待这三个异步操作都完成。当所有的异步操作完成后,Promise.allSettled() 方法返回一个 Promise 对象,该对象带有一个结果数组,其中每个结果表示对应的 Promise 对象的执行结果。

.then() 方法中,我们遍历了结果数组,并根据每个结果的 status 属性来判断对应的 Promise 对象是否成功执行。如果成功执行,则打印出获取数据成功的信息,否则打印出获取数据失败的信息。

function isPromise (val) 
  return typeof val.then === 'function'; // (123).then => undefined


Promise.allSettled = function(promises) 
  return new Promise((resolve, reject) => 
    let arr = [];
    let times = 0;
    const setData = (index, data) => 
      arr[index] = data;
      if (++times === promises.length) 
        resolve(arr);
      
      console.log('times', times)
    

    for (let i = 0; i < promises.length; i++) 
      let current = promises[i];
      if (isPromise(current)) 
        current.then((data) => 
          setData(i,  status: 'fulfilled', value: data );
        , err => 
          setData(i,  status: 'rejected', value: err )
        )
       else 
        setData(i,  status: 'fulfilled', value: current )
      
    
  )

6 实现 Promise.race

race 的实现相比之下就简单一些,只要有一个 promise 执行完,直接 resolve 并停止执行

Promise.race = function(promises) 
  return new Promise((resolve, reject) => 
    let len = promises.length;
    if(len === 0) return;
    for(let i = 0; i < len; i++) 
      Promise.resolve(promise[i]).then(data => 
        resolve(data);
        return;
      ).catch(err => 
        reject(err);
        return;
      )
    
  )

7.实现一个Promise

11 实现发布订阅模式

发布订阅者模式,一种对象间一对多的依赖关系,但一个对象的状态发生改变时,所依赖它的对象都将得到状态改变的通知。

主要的作用(优点):

  1. 广泛应用于异步编程中(替代了传递回调函数)
  2. 对象之间松散耦合的编写代码

缺点:

  • 创建订阅者本身要消耗一定的时间和内存
  • 多个发布者和订阅者嵌套一起的时候,程序难以跟踪维护

实现的思路:

  • 创建一个对象(缓存列表)
  • on方法用来把回调函数fn都加到缓存列表中
  • emit 根据key值去执行对应缓存列表中的函数
  • off方法可以根据key值取消订阅
   class EventEmiter 
            constructor() 

                this._events = 
            
            // 订阅事件的方法
            on(eventName, callback) 
                if (!this._events) 
                    this._events = 
                
                // 合并之前订阅的cb
                this._events[eventName] = [...(this._events[eventName] || []), callback]
            
            // 触发事件的方法
            emit(eventName, ...args) 
                if (!this._events[eventName]) 
                    return
                
                // 遍历执行所有订阅的事件
                this._events[eventName].forEach(fn => 
                    fn(...args)
                );
            
            off(eventName, cb) 
                if (!this._events[eventName]) 
                    return
                
                // 遍历执行所有订阅的事件
                this._events[eventName] = this._events[eventName].filter(fn => fn != cb && fn.l != cb)
            
            once(eventName, callback) 
                const one = (...args) => 
                    // 等callback执行完毕在删除
                    callback(args)
                    this.off(eventName, one)
                
                one.l = callback // 自定义属性
                this.on(eventName, one)
            

        

        let event = new EventEmiter();
        let login1 = function (...args) 
            console.log('login success1', args)
        
        let login2 = function (...args) 
            console.log('login success2', args)
        
        event.on('login', login1)
        // 
        event.once('login', login2)

        event.off('login', login1) // 解除订阅
        event.emit('login', 1, 2, 3, 4, 5)
        event.emit('login', 6, 7, 8, 9)
        event.emit('login', 10, 11, 12)

发布订阅者模式和观察者模式的区别?

  • 发布/订阅模式是观察者模式的一种变形,两者区别在于,发布/订阅模式在观察者模式的基础上,在目标和观察者之间增加一个调度中心。
  • 观察者模式是由具体目标调度,比如当事件触发,Subject 就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的。
  • 发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在。

12.实现观察者模式

观察者模式(基于发布订阅模式) 有观察者,也有被观察者

观察者需要放到被观察者中,被观察者的状态变化需要通知观察者 我变化了 内部也是基于发布订阅模式,收集观察者,状态变化后要主动通知观察者

 class Subject  // 被观察者 学生
            constructor() 
                this.state = 'happy'
                this.observers = []; // 存储所有的观察者
            
            // 收集所有的观察者
            attach(o) 
                this.observers.push(o)
            
            // 更新被观察者 状态的方法
            setState(newState) 
                this.state = newState; // 更新状态
                // this 指被观察者 学生
                this.observers.forEach(o => o.update(this)) // 通知观察者 更新它们的状态
            
        
        class Observer  // 观察者 父母和老师
            constructor(name) 
                this.name = name;
            
            update(student) 
                console.log('当前' + this.name + '被通知了', '当前学生的状态是' + student.state)
            
        
        let student = new Subject('学生');

        let parent = new Observer('父母');
        let teacher = new Observer('老师');
        // 被观察者存储观察者的前提,需要先接纳观察者
        student.attach(parent);
        student.attach(teacher);
        student.setState('被欺负了')

13.实现单例模式

核心要点: 用闭包和Proxy属性拦截

使用闭包和 Proxy 属性拦截可以很好地实现单例模式。具体来说,我们可以通过闭包来确保只创建一个实例,然后使用 Proxy 属性拦截来防止对该实例进行不必要的操作或修改

在实现中,我们可以先将类的构造函数定义为私有属性,并通过闭包来创建一个实例。然后,我们可以使用 Proxy 对象来拦截对该实例的属性读取、赋值和删除操作,以确保只有一个实例并且不被修改。最后,我们可以将实例作为单例对象的公共属性,使其可以被全局访问。

   const Singleton = (function () 
            let instance = null;
            const SingletonClass = function () 
                // ...私有属性和方法
            
            // construct 方法用于拦截 SingletonClass 的构造函数,
            // 它检查是否已经创建了实例,如果没有则创建实例,否则返回已经存在的实例。
            return new Proxy(SingletonClass, 
                construct(target, args) 
                    if (!instance) 
                        instance = new target(...args);
                    
                    return instance;
                ,
                get(target, prop) 
                    return instance[prop];
                ,
                set(target, prop, value) 
                    // 防止修改属性
                    throw new Error('Cannot modify singleton instance');
                ,
                deleteProperty(target, prop) 
                    // 防止删除属性
                    throw new Error('Cannot delete singleton instance property');
                
            )
        )()
        // 使用示例
        const s1 = new Singleton(); // 创建单例实例
        const s2 = new Singleton(); // 获取已有单例实例
        console.log(s1 === s2); // true,两个实例是同一个对象
        s1.foo = 'bar'; // 不能修改属性,会抛出错误
        console.log(s1.foo); // undefined
        delete s1.foo; // 不能删除属性,会抛出错误

14 实现Ajax

步骤

  • 创建 XMLHttpRequest 实例
  • 发出 HTTP 请求
  • 服务器返回 XML 格式的字符串
  • JS 解析 XML,并更新局部页面
  • 不过随着历史进程的推进,XML 已经被淘汰,取而代之的是 JSON。
function ajax() 
            let xhr = new XMLHttpRequest();
            xhr.open('get', 'http://127.0.0.1')
            xhr.onreadystatechange = (request, resonese) => 
                console.log(xhr.readyState)
                if (xhr.readyState === 4) 
                    console.log(xhr.status)
                    if (xhr.status >= 200 && xhr.status < 300) 
                        console.log('成功')
                        let string = xhr.responseText
                        //JSON.parse() 方法用来解析JSON字符串,构造由字符串描述的JavaScript值或对象
                        let object = JSON.parse(string)
                    
                
            
      //参数2,url。参数三:异步
            xhr.send() //用于实际发出 HTTP 请求。不带参数为GET请求
        

Promise实现

基于Promise封装Ajax

  • 返回一个新的Promise实例
  • 创建HMLHttpRequest异步对象
  • 调用open方法,打开url,与服务器建立链接(发送前的一些处理)
  • 监听Ajax状态信息
  • 如果xhr.readyState == 4(表示服务器响应完成,可以获取使用服务器的响应了)
    • xhr.status == 200,返回resolve状态
    • xhr.status == 404,返回reject状态
  • xhr.readyState !== 4,把请求主体的信息基于send发送给服务器
function ajax(url) 
            return new Promise((resolve, reject) => 
                let xhr = new XMLHttpRequest();
                xhr.open('get', url)
                xhr.onreadystatechange = () => 
                    if (xhr.readyState == 4) 
                        if (xhr.status >= 200 && xhr.status <= 300) 
                            resolve(JSON.parse(xhr.responseText))
                         else 
                            reject('请求出错')
                        
                    
                
                xhr.send() //发送hppt请求
            )
        
        let url = '/data.json'
        ajax(url).then(res => console.log(res))
            .catch(reason => console.log(reason))

15 实现JSONP方法

利用<script>标签不受跨域限制的特点,缺点是只能支持 get 请求

  • 创建script标签
  • 设置script标签的src属性,以问号传递参数,设置好回调函数callback名称
  • 插入到html文本中
  • 调用回调函数,res参数就是获取的数据
 function jsonp(
            url,
            params,
            callback
        ) 
            return new Promise((reslove, reject) => 
                let script = document.createElement('script');
                // 只有当服务器返回数据时,才会执行该函数。
                window[callback] = function (data) 
                    reslove(data);
                    document.body.removeChild(script)
                
                let arr = [];
                for (let key in params) 
                    arr.push(`$key=$params[key]`)
                
                script.type = 'text/javascript';
                script.src = `$url?callback=$callback&$arr.join('&')`
                document.body.appendChild(script)
            )
        
        // 测试用例
        jsonp(
            url: 'http://suggest.taobao.com/sug',
            callback: 'getData',
            params: 
                q: 'iphone手机',
                code: 'utf-8'
            ,
        ).then(data => 
            console.log(data)
        )

16.实现async/await

17 基于Generator函数实现async/await原理

18.实现ES6的const

  • enumerable:表示该属性是否可枚举。如果设置为 false,则该属性不会出现在 for...in 循环中,也不会被 Object.keys()Object.values()Object.entries() 等方法返回。

  • configurable:表示该属性是否可配置。如果设置为 false,则该属性不可以使用 delete 运算符删除,也不可以重新定义属性描述符。

var __const = function __const (data, value) 
    window.data = value // 把要定义的data挂载到window下,并赋值value
    Object.defineProperty(window, data,  // 利用Object.defineProperty的能力劫持当前对象,并修改其属性描述符
      enumerable: false,
      configurable: false,
      get: function () 
        return value
      ,
      set: function (data) 
        if (data !== value)  // 当要对当前属性进行赋值时,则抛出错误!
          throw new TypeError('Assignment to constant variable.')
         else 
          return value
        
      
    )
  
  __const('a', 10)
  console.log(a)
  delete a
  console.log(a)
  for (let item in window)  // 因为const定义的属性在global下也是不存在的,所以用到了enumerable: false来模拟这一功能
    if (item === 'a')  // 因为不可枚举,所以不执行
      console.log(window[item])
    
  
  a = 20 // 报错

19 实现一个迭代器生成函数

JS原生的集合类型数据结构,只有Array(数组)和Object(对象);而ES6中,又新增了MapSet。四种数据结构各自有着自己特别的内部实现,但我们仍期待以同样的一套规则去遍历它们,所以ES6在推出新数据结构的同时也推出了一套统一的接口机制——迭代器(Iterator)。

ES6约定,任何数据结构只要具备Symbol.iterator属性(这个属性就是Iterator的具体实现,它本质上是当前数据结构默认的迭代器生成函数),就可以被遍历——准确地说,是被for...of...循环和迭代器的next方法遍历。 事实上,for...of...的背后正是对next方法的反复调用。

在ES6中,针对ArrayMapSetStringTypedArray、函数的 arguments 对象、NodeList 对象这些原生的数据结构都可以通过for...of...进行遍历。原理都是一样的,此处我们拿最简单的数组进行举例,当我们用for...of...遍历数组时:

const arr = [1, 2, 3]
const len = arr.length
for(item of arr) 
   console.log(`当前元素是$item`)

之所以能够按顺序一次一次地拿到数组里的每一个成员,是因为我们借助数组的Symbol.iterator生成了它对应的迭代器对象,通过反复调用迭代器对象的next方法访问了数组成员,像这样:
const arr = [1, 2, 3]
// 通过调用iterator,拿到迭代器对象
const iterator = arr[Symbol.iterator]()

// 对迭代器对象执行next,就能逐个访问集合的成员
iterator.next()
iterator.next()
iterator.next()
 

 而for...of...做的事情,基本等价于下面这通操作:

// 通过调用iterator,拿到迭代器对象
const iterator = arr[Symbol.iterator]()

// 初始化一个迭代结果
let now =  done: false 

// 循环往外迭代成员
while(!now.done) 
    now = iterator.next()
    if(!now.done) 
        console.log(`现在遍历到了$now.value`)
    

可以看出,for...of...其实就是iterator循环调用换了种写法。在ES6中我们之所以能够开心地用for...of...遍历各种各种的集合,全靠迭代器模式在背后给力。

2 实现迭代器生成函数

我们说迭代器对象全凭迭代器生成函数帮我们生成。在ES6中,实现一个迭代器生成函数并不是什么难事儿,因为ES6早帮我们考虑好了全套的解决方案,内置了贴心的生成器Generator)供我们使用:

// 编写一个迭代器生成函数
function *iteratorGenerator() 
    yield '1号选手'
    yield '2号选手'
    yield '3号选手'


const iterator = iteratorGenerator()

iterator.next()
iterator.next()
iterator.next()

 

 
 

 用ES5去写一个能够生成迭代器对象的迭代器生成函数(解析在注释里):

// 定义生成器函数,入参是任意集合
function iteratorGenerator(list) 
    // idx记录当前访问的索引
    var idx = 0
    // len记录传入集合的长度
    var len = list.length
    return 
        // 自定义next方法
        next: function() 
            // 如果索引还没有超出集合长度,done为false
            var done = idx >= len
            // 如果done为false,则可以继续取值
            var value = !done ? list[idx++] : undefined
            
            // 将当前值与遍历是否完毕(done)返回
            return 
                done: done,
                value: value
            
        
    


var iterator = iteratorGenerator(['1号选手', '2号选手', '3号选手'])
iterator.next()
iterator.next()
iterator.next()
 

 20 实现ES6的extends

function extend(subClass, superClass) 
  subClass.prototype = Object.create(superClass.prototype);
  subClass.prototype.constructor = subClass;
  Object.setPrototypeOf(subClass, superClass);
  1. 通过 Object.create(superClass.prototype) 创建一个空对象,将其作为 subClass 的原型对象,以实现原型链继承。
  2. subClass.prototype.constructor 设置为 subClass,因为通过第一步的操作,subClass.prototype 的构造函数已经被设置为 superClass,需要将其修正回来。
  3. 使用 Object.setPrototypeOf(subClass, superClass)subClass 的原型对象指向 superClass,以实现静态方法和属性的继承。
class Person 
  constructor(name) 
    this.name = name;
  

  sayHello() 
    console.log(`Hello, $this.name!`);
  


class Student extends Person 
  constructor(name, grade) 
    super(name);
    this.grade = grade;
  

  study() 
    console.log(`$this.name is studying in grade $this.grade.`);
  


extend(Student, Person);

const john = new Student('John', 5);
john.sayHello(); // "Hello, John!"
john.study(); // "John is studying in grade 5."

21 实现Object.create

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__

// 模拟 Object.create

function create(proto) 
  function F() 
  F.prototype = proto;

  return new F();

22 实现Object.freeze

Object.freeze冻结一个对象,让其不能再添加/删除属性,也不能修改该对象已有属性的可枚举性、可配置可写性,也不能修改已有属性的值和它的原型属性,最后返回一个和传入参数相同的对象

function myFreeze(obj) 
  if (obj instanceof Object) 
    Object.seal(obj);
    for (let key in obj) 
      if (obj.hasOwnProperty(key)) 
        Object.defineProperty(obj, key, 
          writable: false
        )
        myFreeze(obj[key]);
      
    
  
  return obj;

23 实现Object.is

Object.is不会转换被比较的两个值的类型,这点和===更为相似,他们之间也存在一些区别

  • NaN===中是不相等的,而在Object.is中是相等的
  • +0-0在===中是相等的,而在Object.is中是不相等的
 Object.is = function (x, y) 

            if (x === y) 
                // 当前情况下,只有一种情况是特殊的,即 +0 -0
                // 如果 x !== 0,则返回true
                // 如果 x === 0,则需要判断+0和-0,则可以直接使用 1/+0 === Infinity 和 1/-0 === -Infinity来进行判断
                return x !== 0 || 1 / x === 1 / y;
            
            // x !== y 的情况下,只需要判断是否为NaN,如果x!==x,则说明x是NaN,同理y也一样
            // x和y同时为NaN时,返回true
            return x !== x && y !== y;
        

24.实现一个compose函数

组合多个函数,从右到左,比如:compose(f, g, h) 最终得到这个结果 (...args) => f(g(h(...args))).这种组合方式从右向左执行函数,即先执行h函数,再执行g函数,最后执行f函数。最终返回的是一个函数,这个函数可以接受任意参数,将这些参数依次传入第一个函数f,然后依次执行后面的函数,直到最后一个函数执行完毕并返回结果。

题目描述:实现一个 compose 函数,这个意思是将多个函数合并成一个函数,这个合并后的函数会依次执行传入的每个函数,并将前一个函数的执行结果作为后一个函数的输入参数,直到最后一个函数执行完毕并返回结果

// 用法如下:
function fn1(x) 
  return x + 1;

function fn2(x) 
  return x + 2;

function fn3(x) 
  return x + 3;

function fn4(x) 
  return x + 4;

const a = compose(fn1, fn2, fn3, fn4);
console.log(a(1)); // 1+4+3+2+1=11
function compose(...funcs) 
  if (!funcs.length) return (v) => v;

  if (funcs.length === 1) 
    return funcs[0]
  

  return funcs.reduce((a, b) => 
    return (...args) => a(b(...args)))
  

compose创建了一个从右向左执行的数据流。如果要实现从左到右的数据流,可以直接更改compose的部分代码即可实现

  • 更换Api接口:把reduce改为reduceRight
  • 交互包裹位置:把a(b(...args))改为b(a(...args))

25 setTimeout与setInterval实现

1 setTimeout 模拟实现 setInterval

题目描述: setInterval 用来实现循环定时调用 可能会存在一定的问题 能用 setTimeout 解决吗

function mySetInterval(fn, t) 
  let timerId = null;
  function interval() 
    fn();
    timerId = setTimeout(interval, t); // 递归调用
  
  timerId = setTimeout(interval, t); // 首次调用
  return 
    // 利用闭包的特性 保存timerId
    cancel:() => 
      clearTimeout(timerId)
    
  

// 测试
var a = mySetInterval(()=>
  console.log(111);
,1000)
var b = mySetInterval(() => 
  console.log(222)
, 1000)

// 终止定时器
a.cancel()
b.cancel()

为什么要用 setTimeout 模拟实现 setIntervalsetInterval 的缺陷是什么?

setInterval(fn(), N);

上面这句代码的意思其实是fn()将会在 N 秒之后被推入任务队列。在 setInterval 被推入任务队列时,如果在它前面有很多任务或者某个任务等待时间较长比如网络请求等,那么这个定时器的执行时间和我们预定它执行的时间可能并不一致。

// 最常见的出现的就是,当我们需要使用 ajax 轮询服务器是否有新数据时,必定会有一些人会使用 setInterval,然而无论网络状况如何,它都会去一遍又一遍的发送请求,最后的间隔时间可能和原定的时间有很大的出入

// 做一个网络轮询,每一秒查询一次数据。
let startTime = new Date().getTime();
let count = 0;

setInterval(() => 
    let i = 0;
    while (i++ < 10000000); // 假设的网络延迟
    count++;
    console.log(
        "与原设定的间隔时差了:",
        new Date().getTime() - (startTime + count * 1000),
        "毫秒"
    );
, 1000)

// 输出:
// 与原设定的间隔时差了: 567 毫秒
// 与原设定的间隔时差了: 552 毫秒
// 与原设定的间隔时差了: 563 毫秒
// 与原设定的间隔时差了: 554 毫秒(2次)
// 与原设定的间隔时差了: 564 毫秒
// 与原设定的间隔时差了: 602 毫秒
// 与原设定的间隔时差了: 573 毫秒
// 与原设定的间隔时差了: 633 毫秒

再次强调,定时器指定的时间间隔,表示的是何时将定时器的代码添加到消息队列,而不是何时执行代码。所以真正何时执行代码的时间是不能保证的,取决于何时被主线程的事件循环取到,并执行

setInterval有两个缺点

  • 使用setInterval时,某些间隔会被跳过
  • 可能多个定时器会连续执行

可以这么理解:每个setTimeout产生的任务会直接push到任务队列中;而setInterval在每次把任务push到任务队列前,都要进行一下判断(看上次的任务是否仍在队列中)。因而我们一般用setTimeout模拟setInterval,来规避掉上面的缺点。

2 setInterval 模拟实现 setTimeout

const mySetTimeout = (fn, t) => 
  const timer = setInterval(() => 
    clearInterval(timer);
    fn();
  , t);
;


// 测试
// mySetTimeout(()=>
//   console.log(1);
// ,1000)

26 实现Node的require方法

require 基本原理

27 实现LRU淘汰算法

假如我们有一块内存,专门用来缓存我们最近发访问的网页,访问一个新网页,我们就会往内存中添加一个网页地址,随着网页的不断增加,内存存满了,这个时候我们就需要考虑删除一些网页了。这个时候我们找到内存中最早访问的那个网页地址,然后把它删掉。这一整个过程就可以称之为 LRU 算法

 

梳理实现 LRU 思路

  • 特点分析:
    • 我们需要一块有限的存储空间,因为无限的化就没必要使用LRU算发删除数据了。
    • 我们这块存储空间里面存储的数据需要是有序的,因为我们必须要顺序来删除数据,所以可以考虑使用 ArrayMap 数据结构来存储,不能使用 Object,因为它是无序的。
    • 我们能够删除或者添加以及获取到这块存储空间中的指定数据。
    • 存储空间存满之后,在添加数据时,会自动删除时间最久远的那条数据。
  • 实现需求:
    • 实现一个 LRUCache 类型,用来充当存储空间
    • 采用 Map 数据结构存储数据,因为它的存取时间复杂度为 O(1),数组为 O(n)
    • 实现 getset 方法,用来获取和添加数据
    • 我们的存储空间有长度限制,所以无需提供删除方法,存储满之后,自动删除最久远的那条数据
    • 当使用 get 获取数据后,该条数据需要更新到最前面
class LRUCache 
  constructor(length) 
    this.length = length; // 存储长度
    this.data = new Map(); // 存储数据
  
  // 存储数据,通过键值对的方式
  set(key, value) 
    const data = this.data;
    if (data.has(key)) 
      data.delete(key)
    

    data.set(key, value);

    // 如果超出了容量,则需要删除最久的数据
    if (data.size > this.length) 
      const delKey = data.keys().next().value;
      data.delete(delKey);
    
  
  // 获取数据
  get(key) 
    const data = this.data;
    // 未找到
    if (!data.has(key)) 
      return null;
    
    const value = data.get(key); // 获取元素
    data.delete(key); // 删除元素
    data.set(key, value); // 重新插入元素

    return value // 返回获取的值
  

var lruCache = new LRUCache(5);
  • set 方法:往 map 里面添加新数据,如果添加的数据存在了,则先删除该条数据,然后再添加。如果添加数据后超长了,则需要删除最久远的一条数据。data.keys().next().value 便是获取最后一条数据的意思。
  • get 方法:首先从 map 对象中拿出该条数据,然后删除该条数据,最后再重新插入该条数据,确保将该条数据移动到最前面
// 测试

// 存储数据 set:

lruCache.set('name', 'test');
lruCache.set('age', 10);
lruCache.set('sex', '男');
lruCache.set('height', 180);
lruCache.set('weight', '120');
console.log(lruCache);

继续插入数据,此时会超长,代码如下:

lruCache.set('grade', '100');
console.log(lruCache);

 我们使用 get 获取数据,代码如下:

 我们发现此时 sex 字段已经跑到最前面去了

总结

LRU 算法其实逻辑非常的简单,明白了原理之后实现起来非常的简单。最主要的是我们需要使用什么数据结构来存储数据,因为 map 的存取非常快,所以我们采用了它,当然数组其实也可以实现的。还有一些小伙伴使用链表来实现 LRU,这当然也是可以的。

02Spring源码-手写篇-手写DI实现

Spring源码手写篇-手写DI

  简单回顾前面的手写IoC的内容。

一、DI介绍

  DI(Dependency injection)依赖注入。对象之间的依赖由容器在运行期决定,即容器动态的将某个依赖注入到对象之中。说的直白点就是给Bean对象的成员变量赋值。

  在这里我们就需要明白几个问题。

1. 哪些地方会有依赖

  • 构造参数依赖
  • 属性依赖

2. 依赖注入的本质是什么?

  依赖注入的本质是 赋值。赋值有两种情况

  1. 给有参构造方法赋值
  2. 给属性赋值

3. 参数值、属性值有哪些?

  具体赋值有两种情况:直接值和Bean依赖。比如

public clsss Girl
     public Girl(String name,int age,char cup,Boy boyfriend)
         ...
     

4. 直接赋值有哪些?

  • 基本数据类型:String、int 等
  • 数组,集合
  • map

二、构造注入

  我们先来看看构造参数注入的情况应该要如何解决。

1.构造注入分析

  我们应该如何定义构造参数的依赖?也就是我们需要通过构造方法来创建实例,然后对应的构造方法我们需要传入对应的参数。如果不是通过IoC来处理,我们可以直接通过如下的代码实现。

    public static void main(String[] args) 
        Boy boy = new Boy();
        Girl girl = new Girl("小丽",20,'C',boy);
    

  我们通过直接赋值的方式就可以了。但是在IoC中我们需要通过反射的方式来处理。那么我们在通过反射操作的时候就需要能获取到对应的构造参数的依赖了,这时我们得分析怎么来存储我们的构造参数的依赖了。构造参数的依赖有两个特点:

  • 数量
  • 顺序

  上面的例子中的参数

  1. 小丽
  2. 20
  3. ‘C’
  4. boy,是一个依赖Bean

  参数可以有多个,我们完全可以通过List集合来存储,而且通过添加数据的顺序来决定构造参数的顺序了。但是这里有一个问题,如何表示Bean依赖呢?直接值我们直接添加到集合中就可以了,但是Bean依赖,我们还没有创建对应的对象,这时我们可以维护一个自定义对象,来绑定相关的关系。

2. BeanReference

  BeanReference就是用来说明bean依赖的:也就是这个属性依赖哪个类型的Bean

  可以根据name来依赖,也可以按照Type来依赖。当然我们的程序中还有一点需要考虑,就是如何来区分是直接值还是Bean依赖呢?有了上面的设计其实就很容易判断了。

if ( obj instance BeanReference)

  当然还有一种比较复杂的情况如下:

  直接值是数组或者集合等,同时容器中的元素是Bean依赖,针对这种情况元素值还是需要用BeanReference来处理的。Bean工厂在处理时需要遍历替换。

3. BeanDefinition实现

  接下来我们看看如何具体的来实现DI基于构造参数依赖的相关操作。首先是定义的相关处理了。我们需要在 BeanDefinition中增加构造参数的获取的方法。

  然后我们需要在默认的实现GenericBeanDefinition中增加对应的方法来处理。

  定义后我们可以测试下对应的应用,定义个ABean,依赖了CBean

public class ABean 

	private String name;

	private CBean cb;

	public ABean(String name, CBean cb) 
		super();
		this.name = name;
		this.cb = cb;
		System.out.println("调用了含有CBean参数的构造方法");
	

	public ABean(String name, CCBean cb) 
		super();
		this.name = name;
		this.cb = cb;
		System.out.println("调用了含有CCBean参数的构造方法");
	

	public ABean(CBean cb) 
		super();
		this.cb = cb;
	

	public void doSomthing() 
		System.out.println("Abean.doSomthing(): " + this.name + " cb.name=" + this.cb.getName());
	

	public void init() 
		System.out.println("ABean.init() 执行了");
	

	public void destroy() 
		System.out.println("ABean.destroy() 执行了");
	


然后在实例化时我们需要做相关的绑定

		GenericBeanDefinition bd = new GenericBeanDefinition();
		bd.setBeanClass(ABean.class);
		// 定义的构造参数的依赖
		List<Object> args = new ArrayList<>();
		args.add("abean01");
		// Bean依赖 通过BeanReference 来处理
		args.add(new BeanReference("cbean"));
		bd.setConstructorArgumentValues(args);
		bf.registerBeanDefinition("abean", bd);

构造参数传递后,接下来其实我们就需要要在 BeanFactory中来实现构造参数的注入了

4.BeanFactory实现

  前面我们在BeanFactory中实现Bean对象的创建有几种方式

  • 构造方法创建
  • 工厂静态方法
  • 工厂成员方法

  我们在通过构造方法创建其实是通过无参构造方法来处理的,这时我们需要改变这块的逻辑,通过有参构造方法来实现。

	// 构造方法来构造对象
	private Object createInstanceByConstructor(BeanDefinition bd)
			throws InstantiationException, IllegalAccessException 
		try 
			return bd.getBeanClass().newInstance();
		 catch (SecurityException e1) 
			log.error("创建bean的实例异常,beanDefinition:" + bd, e1);
			throw e1;
		
	

  我们就需要对上面的方法做出改变。

	// 构造方法来构造对象
	private Object createInstanceByConstructor(BeanDefinition bd)
			throws InstantiationException, IllegalAccessException 
        // 1. 得到真正的参数值
		List<?> constructorArgumentValues = bd.getConstructorArgumentValues(); 
        // 2.根据对应的构造参数依赖获取到对应的 Constructor 
        Constructor  constructor = 得到对应的构造方法
        // 3.用实际参数值调用构造方法创建对应的对象
        return constructor.newInstance(Object ... 实参值); 
	

  通过上面的分析我们需要获取对应的构造器。这块我们需要通过反射来获取了。下面是具体的实现逻辑

  根据上面的分析,我们实现的逻辑分为两步

  1. 先根据参数的类型进行精确匹配查找,如果没有找到,继续执行第二步操作
  2. 获得所有的构造方法,遍历构造方法,通过参数数量过滤,再比对形参与实参的类型

  因为这里有个情况,实参是Boy,构造方法的形参是Person,第一种精确匹配就没有办法关联了。

具体的实现代码如下:

    private Constructor<?> determineConstructor(BeanDefinition bd, Object[] args) throws Exception 
        /*判定构造方法的逻辑应是怎样的?
        1 先根据参数的类型进行精确匹配查找,如未找到,则进行第2步查找;
        2获得所有的构造方法,遍历,通过参数数量过滤,再比对形参类型与实参类型。
        * */

        Constructor<?> ct = null;

        //没有参数,则用无参构造方法
        if (args == null) 
            return bd.getBeanClass().getConstructor(null);
        

        // 1 先根据参数的类型进行精确匹配查找
        Class<?>[] paramTypes = new Class[args.length];
        int j = 0;
        for (Object p : args) 
            paramTypes[j++] = p.getClass();
        
        try 
            ct = bd.getBeanClass().getConstructor(paramTypes);
         catch (Exception e) 
            // 这个异常不需要处理
        

        if (ct == null) 
            // 2 没有精确参数类型匹配的,获得所有的构造方法,遍历,通过参数数量过滤,再比对形参类型与实参类型。
            // 判断逻辑:先判断参数数量,再依次比对形参类型与实参类型
            outer:
            for (Constructor<?> ct0 : bd.getBeanClass().getConstructors()) 
                Class<?>[] paramterTypes = ct0.getParameterTypes();
                if (paramterTypes.length == args.length)    //通过参数数量过滤
                    for (int i = 0; i < paramterTypes.length; i++)  //再依次比对形参类型与实参类型是否匹配
                        if (!paramterTypes[i].isAssignableFrom(args[i].getClass())) 
                            continue outer; //参数类型不可赋值(不匹配),跳到外层循环,继续下一个
                        
                    

                    ct = ct0;  //匹配上了
                    break outer;
                
            
        

        if (ct != null) 
            return ct;
         else 
            throw new Exception("不存在对应的构造方法!" + bd);
        
    

  上面我们考虑的是BeanFactory通过构造器来获取对象的逻辑,那如果我们是通过静态工厂方法或者成员工厂方法的方式来处理的,那么构造参数依赖的处理是否和前面的是一样的呢?其实是差不多的,我们需要根据对应的构造参数来推断对应的工厂方法

    // 静态工厂方法
    private Object createInstanceByStaticFactoryMethod(BeanDefinition bd) throws Exception 

        Object[] realArgs = this.getConstructorArgumentValues(bd);
        Class<?> type = bd.getBeanClass();
        Method m = this.determineFactoryMethod(bd, realArgs, type);
        return m.invoke(type, realArgs);
    

    // 工厂bean方式来构造对象
    private Object createInstanceByFactoryBean(BeanDefinition bd) throws Exception 

        Object[] realArgs = this.getConstructorArgumentValues(bd);
        Method m = this.determineFactoryMethod(bd, realArgs, this.getType(bd.getFactoryBeanName()));

        Object factoryBean = this.doGetBean(bd.getFactoryBeanName());
        return m.invoke(factoryBean, realArgs);
    

5.缓存功能

  对于上面的处理过程相信大家应该清楚了,我们通过推断也得到了对应的构造方法或者对应的工厂方法,那么我们可以不可以在下次需要再次获取的时候省略掉推导的过程呢?显然我们可以在BeanDefinition中增加缓存方法可以实现这个需求。

6. 循环依赖问题

  上图是循环依赖的三种情况,虽然方式有点不一样,但是循环依赖的本质是一样的,就你的完整创建要依赖与我,我的完整创建也依赖于你。相互依赖从而没法完整创建造成失败。

  我们通过构造参数依赖是完全可能出现上面的情况的,那么这种情况我们能解决吗?构造依赖的情况我们是解决不了的。

public class Test01 

    public static void main(String[] args) 
        new TestService1();
    


class TestService1
    private TestService2 testService2 = new TestService2();


class TestService2
    private  TestService1 testService1 = new TestService1();

  既然解决不了,那么我们在程序中如果出现了,应该要怎么来解决呢?其实我们可以在创建一个Bean的时候记录下这个Bean,当这个Bean创建完成后我们在移除这个Bean,然后我们在getBean的时候判断记录中是否有该Bean,如果有就判断为循环依赖,并抛出异常。数据结构我们可以通过Set集合来处理。

到此构造注入的实现我们就搞定了。

三、属性注入

  上面搞定了构造注入的方式。接下来我们再看看属性注入的方式有什么需要注意的地方。

1. 属性依赖分析

  属性依赖就是某个属性依赖某个值。

public class Girl 

    private String name;
    private int age;
    private char cup;
    private List<Boy> boyFriends;

    // ....

  那么在获取实例对象后如何根据相关的配置来给对应的属性来赋值呢?这时我们可以定义一个实体类 PropertyValue来记录相关的属性和值。

2.BeanDefinition实现

  这时我们就需要在BeanDefinition中关联相关属性信息了。

3.BeanFactory实现

  然后我们在BeanFactory的默认实现DefaultBeanFactory中实现属性值的依赖注入。

// 创建好实例对象
// 给属性依赖赋值
this.setPropertyDIValues(bd,instance);
// 执行初始化相关方法
this.doInit(bd,instance);

  具体的实现代码如下:

    // 给入属性依赖
    private void setPropertyDIValues(BeanDefinition bd, Object instance) throws Exception 
        if (CollectionUtils.isEmpty(bd.getPropertyValues())) 
            return;
        
        for (PropertyValue pv : bd.getPropertyValues()) 
            if (StringUtils.isBlank(pv.getName())) 
                continue;
            
            Class<?> clazz = instance.getClass();
            Field p = clazz.getDeclaredField(pv.getName());
            //暴力访问  private
            p.setAccessible(true);
            p.set(instance, this.getOneArgumentRealValue(pv.getValue()));

        
    

4.循环依赖问题

  在构造参数依赖中我们发现没有办法解决,在属性依赖中同样会存在循环依赖的问题,这时我们能解决吗?

  其实这种情况我们不在IoC场景下非常好解决。如下

Boy b = new Boy();
Girl g = new Girl();
b.setGirl(g);
g.setBoy(b);

  但是在IoC好像不是太好解决:

  针对这种情况我们需要通过 提前暴露来解决这个问题,具体看代码!!!

    private void doEarlyExposeBuildingBeans(String beanName, Object instance) 
        Map<String,Object> earlyExposeBuildingBeansMap = earlyExposeBuildingBeans.get();
        if(earlyExposeBuildingBeansMap == null) 
            earlyExposeBuildingBeansMap = new HashMap<>();
            earlyExposeBuildingBeans.set(earlyExposeBuildingBeansMap);
        
        earlyExposeBuildingBeansMap.put(beanName,instance);
    

最后现阶段已经实现的类图结构

扩展作业:加入Bean配置的条件依赖生效的支持

在Bean定义配置中可以指定它条件依赖某些Bean或类,当这些Bean或类存在时,这个bean的配置才能生效!

以上是关于前端手写篇的主要内容,如果未能解决你的问题,请参考以下文章

前端算法及手写算法JavaScript

JavaScript手撕前端面试题:手写Object.create | 手写Function.call | 手写Function.bind

JavaScript手撕前端面试题:手写new操作符 | 手写Object.freeze

2021前端面试之JavaScript手写题

2021前端面试之JavaScript手写题

前端技能树,面试复习第 52 天—— 手写代码:Javascript 基础考核