Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript专题之惰性函数 #44

Open
mqyqingfeng opened this issue Aug 22, 2017 · 19 comments
Open

JavaScript专题之惰性函数 #44

mqyqingfeng opened this issue Aug 22, 2017 · 19 comments

Comments

@mqyqingfeng
Copy link
Owner

需求

我们现在需要写一个 foo 函数,这个函数返回首次调用时的 Date 对象,注意是首次。

解决一:普通方法

var t;
function foo() {
    if (t) return t;
    t = new Date()
    return t;
}

问题有两个,一是污染了全局变量,二是每次调用 foo 的时候都需要进行一次判断。

解决二:闭包

我们很容易想到用闭包避免污染全局变量。

var foo = (function() {
    var t;
    return function() {
        if (t) return t;
        t = new Date();
        return t;
    }
})();

然而还是没有解决调用时都必须进行一次判断的问题。

解决三:函数对象

函数也是一种对象,利用这个特性,我们也可以解决这个问题。

function foo() {
    if (foo.t) return foo.t;
    foo.t = new Date();
    return foo.t;
}

依旧没有解决调用时都必须进行一次判断的问题。

解决四:惰性函数

不错,惰性函数就是解决每次都要进行判断的这个问题,解决原理很简单,重写函数。

var foo = function() {
    var t = new Date();
    foo = function() {
        return t;
    };
    return foo();
};

更多应用

DOM 事件添加中,为了兼容现代浏览器和 IE 浏览器,我们需要对浏览器环境进行一次判断:

// 简化写法
function addEvent (type, el, fn) {
    if (window.addEventListener) {
        el.addEventListener(type, fn, false);
    }
    else if(window.attachEvent){
        el.attachEvent('on' + type, fn);
    }
}

问题在于我们每当使用一次 addEvent 时都会进行一次判断。

利用惰性函数,我们可以这样做:

function addEvent (type, el, fn) {
    if (window.addEventListener) {
        addEvent = function (type, el, fn) {
            el.addEventListener(type, fn, false);
        }
    }
    else if(window.attachEvent){
        addEvent = function (type, el, fn) {
            el.attachEvent('on' + type, fn);
        }
    }
}

当然我们也可以使用闭包的形式:

var addEvent = (function(){
    if (window.addEventListener) {
        return function (type, el, fn) {
            el.addEventListener(type, fn, false);
        }
    }
    else if(window.attachEvent){
        return function (type, el, fn) {
            el.attachEvent('on' + type, fn);
        }
    }
})();

当我们每次都需要进行条件判断,其实只需要判断一次,接下来的使用方式都不会发生改变的时候,想想是否可以考虑使用惰性函数。

重要参考

Lazy Function Definition Pattern

专题系列

JavaScript专题系列目录地址:https://github.com/mqyqingfeng/Blog

JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

@fi3ework
Copy link

fi3ework commented Nov 3, 2017

function addEvent (type, el, fn) {
    if (window.addEventListener) {
        addEvent = function (type, el, fn) {
            el.addEventListener(type, fn, false);
        }
    }
    else if(window.attachEvent){
        addEvent = function (type, el, fn) {
            el.attachEvent('on' + type, fn);
        }
    }
}

如果我没理解错,这段函数,在第一次执行的时候,addEvent并不会绑定事件,只是对addEvent重新赋值了一次,这样修改如何?

function addEvent (type, el, fn) {
    if (window.addEventListener) {
        el.addEventListener(type, fn, false);
        addEvent = function (type, el, fn) {
            el.addEventListener(type, fn, false);
        }
    }
    else if(window.attachEvent){
        el.attachEvent('on' + type, fn);
        addEvent = function (type, el, fn) {
            el.attachEvent('on' + type, fn);
        }
    }
}

或者立即执行它

@mqyqingfeng
Copy link
Owner Author

@fi3ework 感谢补充哈~ 确实是这样的,第一次并会不绑定事件,所以其实还需要先执行一次:

function addEvent (type, el, fn) {
    if (window.addEventListener) {
        addEvent = function (type, el, fn) {
            el.addEventListener(type, fn, false);
        }
    }
    else if(window.attachEvent){
        addEvent = function (type, el, fn) {
            el.attachEvent('on' + type, fn);
        }
    }
}

addEvent();

然后再使用 addEvent 绑定事件

不过你补充的这种方法非常好,就不用再执行一次了~ o( ̄▽ ̄)d

@forzalianjunting
Copy link

forzalianjunting commented Jul 12, 2018

function addEvent (type, el, fn) {
    if (window.addEventListener) {
        addEvent = function (type, el, fn) {
            el.addEventListener(type, fn, false);
        }
    }
    else if(window.attachEvent){
        addEvent = function (type, el, fn) {
            el.attachEvent('on' + type, fn);
        }
    }
    addEvent(type, el, fn) 
}

其实最后直接执行一次就好了

@lishihong
Copy link

function addEvent (type, el, fn) {
    if (window.addEventListener) {
        addEvent = function (type, el, fn) {
            el.addEventListener(type, fn, false);
        }
    }
    else if(window.attachEvent){
        addEvent = function (type, el, fn) {
            el.attachEvent('on' + type, fn);
        }
    }
    addEvent(type, el, fn) 
}

其实最后直接执行一次就好了

怎么这么多人赞同的啊,这样写不是死循环了吗。。。

@forzalianjunting
Copy link

@lishihong addEvent经过条件判定后已经被重写了

@lishihong
Copy link

@lishihong addEvent经过条件判定后已经被重写了

哦 是啊 明白了

@DarkoPeng
Copy link

感觉用这个例子来引入惰性函数不是很适合

我们现在需要写一个 foo 函数,这个函数返回首次调用时的 Date 对象,注意是首次。

这个需求应该是用 once 函数实现比较好,不过 addEvent 的例子是适合惰性函数的。

function once(fn) {
  var fire, ret
  return function() {
    var self = this
    if (!fire) {
      fire = true
      ret = fn.apply(self, arguments)
    }
    return ret
  }
}

@panyanbin
Copy link

function addEvent (type, el, fn) {
    if (window.addEventListener) {
        addEvent = function (type, el, fn) {
            el.addEventListener(type, fn, false);
        }
    }
    else if(window.attachEvent){
        addEvent = function (type, el, fn) {
            el.attachEvent('on' + type, fn);
        }
    }
    addEvent(type, el, fn) 
}

其实最后直接执行一次就好了

怎么这么多人赞同的啊,这样写不是死循环了吗。。。


如果存在这么一款浏览器

window.addEventListenerwindow.attachEvent都不存在时,就是死循环了吧

@Mcqueengit
Copy link

var foo = function() {  //假设这个是匿名函数A
    var t = new Date();
    foo = function() {   //假设这个是匿名函数B
        return t;
    };
    return foo();
};
foo();   //一个时间
foo();   //同样的事件

如上代码,前后两次执行 foo() 返回同样的时间,我认为应该是利用了闭包的特性。
如果按照您在JavaScript深入之执行上下文中的讲解,那此处的执行过程应该怎么描述呢(注:以下描述只描述到了执行第一个 foo() )。
1.进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈
2.全局执行上下文初始化
接下来就有点不清楚了,是?
3.执行 foo 指向的匿名函数 A ,创建 匿名函数 A 执行上下文,匿名函数 A 执行上下文被压入执行上下文栈
4.匿名函数 A 执行上下文初始化,创建变量对象、作用域链、this等

 anonymousAContext = {
        AO: {
            arguments: {
                length: 0
            },
            t: undefined,
        },
        Scope: [AO,  globalContext.VO],
        this: undefined
}

5.执行 foo 指向的匿名函数 B ,创建 匿名函数 B 执行上下文,匿名函数 B 上下文被压入执行上下文栈
6.匿名函数 B 执行上下文初始化,创建变量对象、作用域链、this等

 anonymousBContext = {
        AO: {
            arguments: {
                length: 0
            },
        },
        Scope: [AO, anonymousAContext.AO, globalContext.VO],
        this: undefined
 }

7.匿名函数 B 执行,沿着作用域链查找 t 值,返回 t 值
8.匿名函数 B 函数执行完毕,匿名函数 B 函数上下文从执行上下文栈中弹出
9.匿名函数 A 函数执行完毕,匿名函数 A 执行上下文从执行上下文栈中弹出

而之所以形成闭包,是因为步骤 4 至步骤 5 中,对 foo 进行了重新赋值,从而让 foo 所指向函数的 [[Scope]] 值为 [ anonymousAContext.AO, globalContext.VO] 。
不知道这样的解释是否正确,对于 foo 是指向匿名函数的变量,之前的教程貌似没有介绍。

@gtandsn
Copy link

gtandsn commented Jan 13, 2020

其实就是foo的指向发生了变化
最开始初始化的时候
foo指向
function () {
var t = new Date()
foo = function () {
console.log(the same time: ${t});
}
return foo();
}
运行完成以后
foo指向
foo = function () {
console.log(the same time: ${t});
}

还有就是一些闭包在起作用

@wubianluoye
Copy link

妙啊

@ferrinweb
Copy link

妙,之前就这样写过,不过就与 const 无缘了。

@anjina
Copy link

anjina commented Dec 13, 2020

妙,之前就这样写过,不过就与 const 无缘了。

改成自执行函数就可以把。

const addEvent = (function(type, el, fn) {
  if(window.addEventListener) {
    return function(type, el, fn) {
      el.addEventListener(type, fn, false);
    }
  }

  if(window.attachEvent) {
    return function(type, el, fn) {
      el.attachEvent('on' + type, fn);
    }
  }
})();

@anjina
Copy link

anjina commented Dec 13, 2020

感觉用这个例子来引入惰性函数不是很适合

我们现在需要写一个 foo 函数,这个函数返回首次调用时的 Date 对象,注意是首次。

这个需求应该是用 once 函数实现比较好,不过 addEvent 的例子是适合惰性函数的。

function once(fn) {
  var fire, ret
  return function() {
    var self = this
    if (!fire) {
      fire = true
      ret = fn.apply(self, arguments)
    }
    return ret
  }
}

个人觉得惰性函数是 通过改写函数来 避免多次做不必要的判断, once 函数 还是需要每次执行都进行判断

@cw84973570
Copy link

学习了,这两篇都简单了好多,之前看柯里化看得头大

@dongxiaosun
Copy link

dongxiaosun commented May 10, 2021

element-ui 处理 dom 事件的源码就是这样写的。

export const on = (function() {
  if (!isServer && document.addEventListener) {
    return function(element, event, handler) {
      if (element && event && handler) {
        element.addEventListener(event, handler, false);
      }
    };
  } else {
    return function(element, event, handler) {
      if (element && event && handler) {
        element.attachEvent('on' + event, handler);
      }
    };
  }
})();

@BlueStoneQ
Copy link

@fi3ework @mqyqingfeng
我是这样改了一下:

function addEvent (type, el, fn) {
  if (window.addEventListener) {
    // 在第一次调用时 根据特征进行重新定义 
    addEvent = function(type, el, fn) {
      el.addEventListener(type, fn, false);
    }
  }

  if (window.attachEvent) {
    addEvent = function(type, el, fn) {
      el.attachEvent('on' + type, fn);
    }
  }

  // me: 我觉得应该有调用这一句,此时调用的已经是重新定义过的addEvent 不会无限递归
  addEvent(type, el, fn);
}

@ferrinweb
Copy link

妙,之前就这样写过,不过就与 const 无缘了。

改成自执行函数就可以把。

const addEvent = (function(type, el, fn) {
  if(window.addEventListener) {
    return function(type, el, fn) {
      el.addEventListener(type, fn, false);
    }
  }

  if(window.attachEvent) {
    return function(type, el, fn) {
      el.attachEvent('on' + type, fn);
    }
  }
})();

这样就不是惰性函数了。

@YuFengjie97
Copy link

妙妙妙

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests