【js进阶6】一些模拟实现

JavaScript | 2020-07-27 20:12:00 823次 1次

一、new

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。在模拟之前先看下用不用 new 关键字的一个区别,之前的文章中有介绍对象实例的 constructor 属性指向创建此实例的构造器:

function Foo(){
  console.log(this.constructor == Foo)
    //也可以使用 instanceof 关键字判断
    console.log(this instanceof Foo)
}

new Foo() // true
Foo() // false

new 的过程中会发生以下事情(依据之前那张原型关系图):

1. 创建一个新对象,原型(__proto__)指向构造函数的原型对象(Foo.prototype),且原型上 constructor 指向构造函数本身

2. 将 this 绑定到新创建的对象。两种调用方式:new Foo 和 new Foo() 效果一样

3. 由构造函数返回的对象就是 new 表达式的结果。如果构造函数没有显式返回一个对象,则使用步骤1创建的对象

//构造函数
function Foo(arg){
    this.a = 1
    this.b = 2
    this.c = arg
    return {
        d: 1
    }
}

//模拟 new
function New(Con) {
    return (...arg) => {
        // 1、创建新对象 __proto__ --> Con.prototype
        const obj = Object.create(Con.prototype);
        // 2、绑定 this
        //const ret = Con.apply(obj, arg);
        const ret = Con.call(obj, ...arg);
        // 3、构造函数中是否有返回值
        return ret instanceof Object ? ret : obj;
    }
};

//使用
let foo = New(Foo)(3)


二、apply/call

这两个方法可以改变 this 指向,this 指向的改变有四种方式:默认绑定、隐式绑定(根据调用关系)、显示绑定(callapplybind)、new 绑定。

var value = 1;
let foo = {
    value: 2
};

function bar(arg) {
    console.log(this.value);
}
bar() // 1
bar.call(foo, 1, 2); // 2
bar.apply(foo, [ 1, 2 ]); // 2

call apply 绑定时改变 this 指向并且会函数立即执行,相当于下面这种效果:

let foo = {
    value: 2,
    bar: () => {
        console.log(this.value)
    }
};
foo.bar() // 2

按照这种思路可以实现如下:

Function.prototype.callSelf = function(context, ...arg) {
  context.fn = this;
  //考虑参数
  let result = context.fn(...arg);
  // 给 foo 挂载后需要删除掉,要的就是执行那一下
  delete context.fn
  return result;
}

还要注意当 call 中第一个参数为 null 或者 undefined 时候,this 会指向 window,参数为基本数据类型时候会被转为对象:

var value = 1;

function bar(...arg) {
    console.log(this, arg);
}

bar.call(null); // window

bar.call(true); // Boolean {true}

所以上述实现中还需要加一个参数处理,使用 Object 包裹下:

Function.prototype.callSelf = function(context, ...arg) {
  context = context ? Object(context) : window; 
  ...
}

同理,apply 的实现上就是传参方式不一样,call 接受多个单独的参数,apply 接受一个数组参数,后面传入多个也无效,所以就改下函数调用那里传参方式就行:

Function.prototype.applySelf = function(context, arg) {
  context = context ? Object(context) : window; 
  context.fn = this;
  //考虑参数
  let result = context.fn(...arg);
  // 给 foo 挂载后需要删除掉,要的就是执行那一下
  delete context.fn
  return result;
}

再来看一个经典的题目:

function fn(a,b){
    console.log(this);
    console.log(a);
    console.log(a+b);
}
fn.call(1);       // 1,undefined,NaN
fn.call.call(fn); // window,undefined,NaN
fn.call.call.call(fn,1,2);    //  1,2,NaN
fn.call.call.call.call(fn,1,2,3); //  1,2,5

多个call调用时,第一个参数必须为函数执行,第二个参数作为this指向,其余参数作为实参传入第一个函数
相当于fn.call.call(fn) ==> Function.prototype.call.call(fn) ==> fn.call()  再多也是同理


三、bind

1. bind 返回一个新函数,this 指向传入的第一个参数,偏函数

2.  返回的新函数可以使用 new 操作,并且 this 指向不再是传入的那个参数

先借助 apply(或者call) 实现第一步:

Function.prototype.bindSelf = function(context, ...arg){
    return (...innerArg) => this.apply(context, [ ...arg, ...innerArg ])
}
//测试
const val = 'window';

let foo = {
    val: 'foo'
};

function test(name, age) {
    // this.val = 'test' 这里如果修改,foo中的val会变成 test  所以证明此时this是指向 foo
    return {
        val: this.value,
	name,
	age
  }
};

let bindFoo = test.bindSelf(foo, "gitSu");
console.log(bindFoo(20))
// {val: "foo", name: "gitSu", age: 20}

继续第二步,可以通过 new 运算对生成的函数进行实例生成,先看下效果:

...
console.log(new bindFoo(20))
// {val: undefined, name: "gitSu", age: 20}
可以看到此时 val 为 undefined,说明也没有访问到 window上的val值

//为了效果更明显,给 test 增加一个原型属性
test.prototype.val = 'test'

此时再打印数据为:{val: "test", name: "gitSu", age: 20}

这种原理应该怎么实现呢,回顾下上面的 new 模拟实现方式,先从本文开篇处可以看到判断方法是否被 new 实例有两种方法(constructor、instanceof)再考虑下如何转换原型链的指向,搞清楚这点基本上就清晰了:

Function.prototype.bindSelf = function(context, ...arg){
    const self = this;
    let inner = function(...innerArg){
        // 判断是否 new 调用
        return self.apply(
            this instanceof inner ? this : context,
            [...arg, ...innerArg]
        )
    }
    // 原型上方法也需要
    inner.prototype = this.prototype;
    return inner;
}


四、Object.creat

这个方法创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__。借助一个空函数实现:

function Create (proto) {
    var F = function () {};
    F.prototype = proto;
    return new F();
}


五、instanceof


instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上,在原型和继承那篇文章中使用过。

function instance(left,right){
    let prototype = right.prototype;
    let proto = Object.getPrototypeOf(left); //__proto__
    while(true){
        //按照上篇文章中那张图,到终点了指向null 或者找不到原型
       if (proto === null || proto === undefined){
           return false;
       }
       if (proto === prototype){
           return true;
       }
       //沿着链 继续往下找
       proto = Object.getPrototypeOf(proto);
    }
}
console.log(instance({},Object)); //true
console.log(instance([],Number)); //false


六、parseInt

上面的几个方法主要是原型相关的一些东西,接下来的几个点没有连贯性了,纯属的代码实现。

parseInt(string, radix)  将一个字符串 string 转换为 radix 进制的整数, radix 为介于 2-36 之间的数。换句话说就是第一个参数必须是第二个参数指定的进制。这个方法 MDN 上有详细的介绍,这里只简单介绍的实现步骤:

1. 参数 1 只能是字符串或数组类型 否则返回 NaN

2. 取小数点之前的位

3. radix 为数字,并且默认指定为 10,范围在 [2 , 16]

4. 只取参数的数字部分,但是大于 10 进制的,需要按照进制转 A-F 为数字,无法转换则返回 NaN

5. 计算结果并返回

function _parseInt(str, radix) {
 let str_type = typeof str;
 
 if (str_type !== 'string' && str_type !== 'number') {
  // 1 如果类型不是 string 或 number 类型返回NaN
  return NaN
 }
 
 // 2 取小数点之前的位
 str = String(str).trim().split('.')[0]
 let length = str.length;
 if (!length) {
  return NaN
 }
 
 if (!radix) {
  // 3 默认10
  radix = 10;
 }
 if (typeof radix !== 'number' || radix < 2 || radix > 36) {
  return NaN
 }

 // 4. 只取参数的数字部分,但是大于 10 进制的,需要按照进制转 A-F 为数字,无法转换则返回 NaN
 let newStr = str.split(''), strArr = [];
 for (let i = 0; i < newStr.length; i++) {
    // charCode : 0-9为[48-57],A-F为[65-70]
    const charCode = newStr[i].toUpperCase().charCodeAt()
    let num;
    if(charCode >= 65){
	num = charCode - 55
    }else{
        num = charCode - 48
    }
    if(num > radix){
    	break;
    }else{
        strArr.push(num)
    }
 }
 let len = strArr.length;
 // 无法转换则返回 NaN
 if(!len ){
     return NaN;
 };
 
 let res = 0;
 // 根据进制 计算结果
 for(let i = 0; i < len; i++) {
     const num = strArr[i] * Math.pow(radix, len - i - 1)
     res += num;
 }
 return res;
}

console.log(_parseInt('AF', 16)) // 175
console.log(parseInt("AF", 16))  // 175
console.log(_parseInt(8, 9))     // 8
console.log(parseInt(8, 9))      // 8


七、reduce

reduce 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。

Array.prototype._reduce = function (fn, initialValue) {
  // 第一个参数不是函数 抛出错误
  if (typeof fn !== 'function') {
    throw new TypeError(fn + ' is not a function');
  }
  // 如果数组为空,且初始值不存在 则抛出错误
  if (!arr.length && !initialValue) {
    throw new TypeError(' Reduce of empty array with no initial value');
  }
  let result = initialValue || 0;
  for (let i = 0, len = this.length; i < len; i++) {
    // 本次返回的值继续传给下一轮
    result = fn.call(this, result, this[i], i, arr)
  }
  return result
}

// test
let arr = [1,2,3]
arr._reduce((total, cur, index, arr) => {
    return total+=cur
}, 1)
// 7


八、map

map 方法不修改原数组,返回一个新数组,第二个参数代表当前数组对象。

Array.prototype._map = function (fn, thisValue) {
  let arr = thisValue || this;
  let result = [];
  if (typeof fn !== 'function') {
    throw new TypeError(fn + ' is not a function');
  }
  for (let i = 0, len = arr.length; i < len; i++) {
    let r = fn.call(arr, arr[i], i, arr)
    result.push(r)
  }
  return result
}
 
let arr = [1,2,3,4];
let result1 = arr._map((item) => {
    return item * 2
})
console.log(result1) // [2,4,6,8]

let result2 = arr._map((item) => {
    return item * 2
}, [2,4,6])
console.log(result2) //指定第二个参数  [4, 8, 12]


九、防抖|节流

函数防抖:短时间内多次触发同一事件,只执行最后一次,或者只执行最开始的一次,中间的不执行。

// 时间间隔默认 1000,返回一个函数 注意 this一定要绑定正确
function debounce(fn, interval = 1000) {
  let timer;
  return function (...arg) {
    clearTimeout(timer);
    timer = setTimeout( () => {
      fn.call(this, ...args);
    }, interval);
  };
}

函数节流:指连续触发事件但是在 n 秒中只执行一次函数。

function throttle(fn, interval = 300) {
  let enterTime = 0;//触发的时间
  return function(...arg) {
    let backTime = new Date();//第一次函数return即触发的时间
    if (backTime - enterTime > interval ) {
      fn.call(this, ...arg);
      enterTime = backTime;
    }
  };
}

函数节流进阶:初始触发,结束也保证触发。

let throttle = function(func, delay){
    let timer = null;
    let startTime = Date.now();

    return function(){
        let curTime = Date.now();
        let remaining = delay - (curTime - startTime);
        let context = this;
        let args = arguments;

        clearTimeout(timer);
        if(remaining<=0){
            func.apply(context,args);
            startTime = Date.now();
        }else{
            timer = setTimeout(func,remaining);
        }
    }
}



十、ajax

//简单的复习下
// 序列化参数
let urlParmsHandler = (param) => {
	let str = '';
    Object.entries(param).map(item => {
        str += `${item[0]}=${item[1]}&`;
    })
    return str.replace(/\&$/g, '')
}

function ajax({url, data, method = 'post', async = true}) {
    return new Promise((resolve, reject) => {
        // 1 创建XMLHttqRequest
        let xhr = new XMLHttpRequest();
		
        // 2、异步请求监听
        xhr.onreadystatechange = function() {
            if (xhr.readyState ==4 ) {
                if(xhr.status == 200) {
                    resolve(xhr.responseText)
                }else{
                    reject(xhr.statusText)
                }
            }
        }
        // 3、初始化请求参数,还没发送请求
        xhr.open(method, url, async);
        xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
        // 4、发起请求
        xhr.send(urlParmsHandler(data))
    })
}

// 使用
ajax({
    url: 'https://www.baidu.com',
    data: {a: 1, b: 2}
}).then(res=>{
    cosnole.log(res)
}).catch(err => {
    console.log(err)
})


十一、观察者模式

在观察者模式中,观察者需要直接订阅目标事件。在目标发出内容改变的事件后,直接接收事件并作出响应。

class Subject{
  constructor(){
    this.subs = [];
  }
  addSub(sub){
    this.subs.push(sub);
  }
  notify(){
    this.subs.forEach(sub=> {
      sub.update();
    });
  }
}

class Observer{
  update(){
    console.log('update');
  }
}

let subject = new Subject();
let ob = new Observer();
//目标添加观察者了
subject.addSub(ob);
//目标发布消息调用观察者的更新方法了
subject.notify();   //update


十二、发布订阅模式

发布订阅模式相比观察者模式多了个事件通道,订阅者和发布者不是直接关联的。订阅者 A 和发布者 B 是通过 pubsub 这个对象关联起来的,他们没有直接的交流。

class Subscribe{
    constructor(){
        this.tiopics = {};
    }
    subscribe(name, fn) {
        if(!this.tiopics[name]){
            this.tiopics[name] = [];
        }
        this.tiopics[name].push(fn);
    }
    publish(name, ...arg) {
        this.tiopics[name].forEach(item => item(...arg))
    }
}

let pubsub = new Subscribe;
// 订阅事件 frist
pubsub.subscribe('frist',function(a,b){
  console.log(a,b);    
});
// 订阅事件 frist 允许注册多次
pubsub.subscribe('frist',function(a,b){
  console.log(a,b);    
});
// 订阅事件 second
pubsub.subscribe('second',function(a,b){
  console.log(a,b);    
});

//发布事件
pubsub.publish('frist','hello','word');
pubsub.publish('second','hello1','word1');


1人赞

分享到: