实现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(字符串)
实现模板引擎
例子:
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;
}