JavaScript学习笔记--实现原理

127 阅读7分钟

学习自该掘金文章

实现Event(event bus)

是前端组件通信的依赖手段之一,属于订阅-发布消息模式(绑定-触发事件)

class EventEmitter{
    constructor(){
        this.event=this.event||new Map();//存储事件类型-回调函数(k-v)
        this.maxListeners=this.maxListeners||10; //设定监听上限
    }
}
//监听事件名为type的事件
EventEmitter.prototype.addEventListener=function(type,fn){
    //获取对应事件已有的回调
    let handle=this.event.get(type);
    //如果没有绑定过监听
    if(!handle){
        this.event.set(type,fn);
    }
//如果已经绑定过回调,且类型是函数,则说明只有一个监听者    
    else if(handle && typeof handle==='function'){
        this.event.set(type,[handle,fn])
        //将该事件的回调设为数组,存储原来和现在的回调
    }
//如果已经绑定过回调,且类型不是函数,说明是数组
    else if(handle.length<=this.maxListeners){
        handle.push(fn);//直接将最新的回调加入
    }
}

//触发事件名为type的事件
EventEmitter.prototype.emit=function(type,...args){
let handle=this.event.get(type);
    if(typeof handle==='function'){//是函数类型,则只有一个监听
        if(args.length!=0){
            handle.apply(this,args);
        }else{
            handle.call(this);
        }
    }else if(Array.isArray(handle)){
    //是数组类型则说明有多个监听者,需要依次触发里面的函数
        for(let i=0;i<handle.length;i++){
            if(args.length!=0){
                handle[i].apply(this,args);
            }else{
                handle[i].call(this);
            }
        }
    }
    return true;
}

//移除事件名为type的事件
EventEmitter.prototype.removeListener=function(type,fn){
    let handle=this.event.get(type);
    //如果类型是函数,则只被监听一次,直接删除该类型
    if(handle && typeof handle==='function'){
        this.event.delete(type,fn)
    }
    //如果类型是数组,说明有多个监听者,需要找到对应的函数
    else{
        let position=-1;
        for(let i=0;i<handle.length;i++){
            if(handle[i]===fn){
                position=i;
                break;
            }
        }
        if(position!=-1){
            handle.splice(position,1);
            //如果清除后只剩下一个函数,则取消数组,直接以函数形式保存
            if(handle.length===1){
            this.event.set(type,handle[0])
            }
        }else{
            return true;
        }
    }
}

实现instanceOf

a instanceOf A:判断a的原型链上是否有A的原型对象

function instanceOf(L,R){
    //L指左表达式,R指右表达式
    while(true){
        if(!L.__proto__) return false;
        if(L.__proto__===R.prototype) return true;
        L=L.__proto__;
    }
}

实现call

call:使用一个指定的this值和若干个指定的参数指的前提下调用某个函数和方法

例子说明:

let obj={
    value:1
}
function f(){
    consolie.log(this.value);
}
f.call(obj);    //1
  • call改变了this的指向,指向obj
  • f函数执行了

如何模拟实现上述效果?把obj对象改造如下:

let obj={
    value:1,
    f:function(){
        consolie.log(this.value);
    }
}
obj.f();    //1

这个时候调用函数f时,this就指向了obj。

但这样却给对象obj本身添加了一个属性,所以在调用之后用delete删除属性即可

步骤如下:

  • 将函数设为对象的属性
  • 对象调用函数
  • 删除该函数
obj.fn=f;   //随意起一个属性名
obj.fn();   //调用
delete obj.fn;  //删除属性

还要考虑为函数传递参数的情况,因为可能传也可能不传,所以可以使用剩余参数

Function.prototype.call_=function(obj,...arg){
    obj=obj||window; //若obj为null时,视为指向window
    obj.fn=this; //this获取调用call的函数
    if(arguments.length>1){
        obj.fn(...arr);
    }else{
        obj.fn()
    }
    delete obj.fn;
}

还要考虑一个情况,如果调用的函数是有返回值的,我们还需要将之返回,如果只在内部调用却没有return出来,那么外面是获取不到值的。

Function.prototype.call_=function(obj){
    obj=obj||window; //若obj为null时,视为指向window
    obj.fn=this; //this获取调用call的函数
    let args=[];
    for(let i=1;i<arguments.length;i++){//从索引为1开始,跳过obj
        args.push(arguments[i])
    }
    let result=obj.fn(...args);
    delete obj.fn;
    return result;
}

当绑定的this是null时,默认this是指向window

let value1='let';//使用let/const/class定义的变量不属于全局变量window,而是存在于一个块级作用域script,在全局访问时直接通过属性名
var value2='var';
let obj={
    value:1
}
function f(){
    consolie.log(this.value1);
    console.log(this.value2);
}
f.call(null);    //undefined  var
console.log(value1);    //let

实现apply

apply与call类似,差别是在为函数传参的不同,使用apply只能有一个参数用来传递函数的参数,当然也要考虑不传参的情况

Function.prototype.apply_=function(obj,arr){
    obj=obj||window;
    obj.fn=this;
    let result;
    if(!arr){
       result=obj.fn() 
    }else{
        let args=[];
        for(let i=0;i<arr.length;i++){
            args.push(arr[i])
        }
        result=obj.fn(...args);
    }
    delete obj.fn;
    return result;
}
//或者使用arguments判断
Function.prototype.apply_=function (context) {
    context=context||window;  //当context是null时,this绑定到window上
    context.fn=this;
    if(!arguments[1]){
      context.fn()
    }else{
       context.fn(...arguments[1]);
    }
}

实现bind

bind()方法会返回一个新函数。当这个新函数被调用时,实际就是执行调用bind的函数,bind()的第一个参数将作为它运行时的this,如果bind()还有其他参数,那么这些参数将在该函数传递的实参前传入,一起作为函数的实参

举例说明bind()的作用

let obj={
    value:1
}
function f(){
    console.log(this.value);
}
let fn=f.bind(obj);
fn();   //1

模拟实现bind()返回函数:

关于指定this的指向,可以使用call或apply实现

Function.prototype.bind=function(context){
    let fn=this; //调用bind的函数
    return function(){
        return fn.call(context)//return的原因是考虑到绑定的函数可能是有返回值的
    }
}

模拟实现bind()的传参

函数使用bind时,可以传参,执行返回的函数时,也可以传参

例子说明

let obj={
    value:1
}
function f(a,b){
    console.log(this.value);
    console.log(a);
    console.log(b);
}
let fn=f.bind(obj,'a');
fn('b');  //1 a b

函数在需要传递两个参数时,可以分开两次传递,一次在使用bind()只传一个,在执行返回的函数时,再传一个。因此我们可以使用arguments来处理

实现传参

Function.prototype.bind=function(context){
    let fn=this;
    let args=Array.prototype.slice.call(arguments,1);  //获取函数实参的第二位到最后一位,因为arguments是类数组,所以要使用call来将类数组指定为slice()函数的this,从而能够使用数组的方法slice()
    return function(){
        let arr=Array.prototype.slice.call(arguments);//将调用返回函数传递的实参全部收集
        return fn.apply(context,args.concat(arr))
        //记住要return,因为函数可能有返回值
        
    }
}

作为构造函数调用的模拟实现

最难的部分:当将返回的函数以构造函数的形式调用时,之前通过bind绑定的this的指向将失效,变成指向实例出来的对象,但为函数传入的实参仍有效

举例:

var value='window';
var obj={
    value:1
}
function f(name,age){
    this.habit='play';
    console.log(this.value);
    console.log(name);
    console.log(age);
}
f.prototype.friend='rose';
let fn=f.bind(obj,'jack');
let o=new fn(10);   //undefined jack 10
console.log(o.habit);   //play
console.log(o.friend);  //rose

尽管调用bind绑定了this为obj,但最后返回了undefined,说明绑定到obj的this已失效,此时指向了o

实现:

Function.prototype.bind=function(context){
    let self=this;
    let args=Array.prototype.slice.call(arguments,1);
    let fn=function(){
    //当这个函数作为构造函数时,this是指向创建出来的实例,则this instanceof fn=true 原型链的继承
        let arr=Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fn? this:context, args.concat(arr))
    }
    //将返回函数的prototype指向调用bind的函数的prototype,这样创建的实例就能继承函数原型上的属性方法
    fn.prototype=self.prototype;
    return fn;  //将函数返回
}

但是这个写法直接将fn.prototype=self.prototype,那当为fn.prototype添加属性时,也会影响绑定函数的原型,因此我们可以借助寄生组合式继承中的寄生方法,使用一个空函数来中转

改进

Function.prototype.bind=function(context){
    let self=this;
    let args=Array.prototype.slice.call(arguments,1);
    let fn=function(){
        let arr=Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fn? this:context, args.concat(arr));
    }
    fn.prototype=Object.create(self.prototype);
    return fn;
}

还要考虑调用bind的对象不是函数的情况

最终实现

Function.prototype.bind=function(context){
    if(typeof this!=='function'){
        //报错
        throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }
    let self=this;
    let args=Array.prototype.slice.call(arguments,1);
    let fn=function(){
        let arr=Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fn? this:context, args.concat(arr));
    }
    fn.prototype=Object.create(self.prototype);
    return fn;
}

实现JSON.parse

将json字符串转为JavaScript的对象

实现

let json='{name:"xx",age:10}'
let obj=eval('('+json+')');

解析URL Params为对象

举例:

let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';
parseParam(url);
/*
结果:
{user:'anonymous',
 id:[123,456],  //重复出现的key要组装成数组,能转成数字的就转成数字类型
 city:'北京',//中文需解码
 enabled:true   //未指定值的key值默认未true
*/

使用正则表达式将‘?’后面的字符串筛选出来

function parseParam(url){
    let result=/.+\?(.+)$/.exec(url)[1];//获取第一个子表达式相匹配的文本,即()里的
    //exec()返回的是一个数组,第一个元素为与正则匹配的文本,第二个元素为与正则的第一个子表达式匹配的文本
    let obj={};
    result=result.split('&');
    result.forEach(item=>{
        if(/=/.test(item)){//处理有value的参数
            let [key,val]=item.split('=');
            val=decodeURIComponent(val);//解码
            val=/^\d+$/.test(item)? parseFloat(val):val;//判断是否要要转为数字
            
            if(obj.hasOwnProperty(key)){//如果对象已有key,则将值变成数组类型,将已有的值和新的val放入
                obj[key]=[].concat(obj[key],val)
            }else{
                obj[key]=val;
            }
        }else{//处理没有值的key
            obj[item]=true;
        }
    })
    return obj;
}

函数exec与match类似,正则.exec(字符串),字符串.match(正则),正则.text(字符串)

函数exec的用法 举例

实现模板引擎

例子:

let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let data = {
  name: 'jack',
  age: 18,
  sex:'f'
}
render(template, data); // 我是jack,年龄18,性别f

实现:

function render(template,data){
    let reg=/\{\{(\w+)\}\}/;
    while(template.match(reg)){
        let key=reg.exec(template)[1]; //获取正则表达式的第一个子表达式匹配的文本
        template=template.replace(reg,data[key]) //把整个{{key}}替换掉
    }
    return template;
}