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专题之jQuery通用遍历方法each的实现 #40

Open
mqyqingfeng opened this issue Aug 3, 2017 · 12 comments
Open

JavaScript专题之jQuery通用遍历方法each的实现 #40

mqyqingfeng opened this issue Aug 3, 2017 · 12 comments

Comments

@mqyqingfeng
Copy link
Owner

mqyqingfeng commented Aug 3, 2017

each介绍

jQuery 的 each 方法,作为一个通用遍历方法,可用于遍历对象和数组。

语法为:

jQuery.each(object, [callback])

回调函数拥有两个参数:第一个为对象的成员或数组的索引,第二个为对应变量或内容。

// 遍历数组
$.each( [0,1,2], function(i, n){
    console.log( "Item #" + i + ": " + n );
});

// Item #0: 0
// Item #1: 1
// Item #2: 2
// 遍历对象
$.each({ name: "John", lang: "JS" }, function(i, n) {
    console.log("Name: " + i + ", Value: " + n);
});
// Name: name, Value: John
// Name: lang, Value: JS

退出循环

尽管 ES5 提供了 forEach 方法,但是 forEach 没有办法中止或者跳出 forEach 循环,除了抛出一个异常。但是对于 jQuery 的 each 函数,如果需要退出 each 循环可使回调函数返回 false,其它返回值将被忽略。

$.each( [0, 1, 2, 3, 4, 5], function(i, n){
    if (i > 2) return false;
    console.log( "Item #" + i + ": " + n );
});

// Item #0: 0
// Item #1: 1
// Item #2: 2

第一版

那么我们该怎么实现这样一个 each 方法呢?

首先,我们肯定要根据参数的类型进行判断,如果是数组,就调用 for 循环,如果是对象,就使用 for in 循环,有一个例外是类数组对象,对于类数组对象,我们依然可以使用 for 循环。

更多关于类数组对象的知识,我们可以查看《JavaScript专题之类数组对象与arguments》

那么又该如何判断类数组对象和数组呢?实际上,我们在《JavaScript专题之类型判断(下)》就讲过jQuery 数组和类数组对象判断函数 isArrayLike 的实现。

所以,我们可以轻松写出第一版:

// 第一版
function each(obj, callback) {
    var length, i = 0;

    if ( isArrayLike(obj) ) {
        length = obj.length;
        for ( ; i < length; i++ ) {
            callback(i, obj[i])
        }
    } else {
        for ( i in obj ) {
            callback(i, obj[i])
        }
    }

    return obj;
}

中止循环

现在已经可以遍历对象和数组了,但是依然有一个效果没有实现,就是中止循环,按照 jQuery each 的实现,当回调函数返回 false 的时候,我们就中止循环。这个实现起来也很简单:

我们只用把:

callback(i, obj[i])

替换成:

if (callback(i, obj[i]) === false) {
    break;
}

轻松实现中止循环的功能。

this

我们在实际的开发中,我们有时会在 callback 函数中用到 this,先举个不怎么恰当的例子:

// 我们给每个人添加一个 age 属性,age 的值为 18 + index
var person = [
    {name: 'kevin'},
    {name: 'daisy'}
]
$.each(person, function(index, item){
    this.age = 18 + index;
})

console.log(person)

这个时候,我们就希望 this 能指向当前遍历的元素,然后给每个元素添加 age 属性。

指定 this,我们可以使用 call 或者 apply,其实也很简单:

我们把:

if (callback(i, obj[i]) === false) {
    break;
}

替换成:

if (callback.call(obj[i], i, obj[i]) === false) {
    break;
}

关于 this,我们再举个常用的例子:

$.each($("p"), function(){
   $(this).hover(function(){ ... });
})

虽然我们经常会这样写:

$("p").each(function(){
    $(this).hover(function(){ ... });
})

但是因为 $("p").each() 方法是定义在 jQuery 函数的 prototype 对象上面的,而 $.each()方法是定义 jQuery 函数上面的,调用的时候不从复杂的 jQuery 对象上调用,速度快得多。所以我们推荐使用第一种写法。

回到第一种写法上,就是因为将 this 指向了当前 DOM 元素,我们才能使用 $(this)将当前 DOM 元素包装成 jQuery 对象,优雅的使用 hover 方法。

所以最终的 each 源码为:

function each(obj, callback) {
    var length, i = 0;

    if (isArrayLike(obj)) {
        length = obj.length;
        for (; i < length; i++) {
            if (callback.call(obj[i], i, obj[i]) === false) {
                break;
            }
        }
    } else {
        for (i in obj) {
            if (callback.call(obj[i], i, obj[i]) === false) {
                break;
            }
        }
    }

    return obj;
}

性能比较

我们在性能上比较下 for 循环和 each 函数:

var arr = Array.from({length: 1000000}, (v, i) => i);

console.time('for')
var i = 0;
for (; i < arr.length; i++) {
    i += arr[i];
}
console.timeEnd('for')


console.time('each')
var j = 0;
$.each(arr, function(index, item){
    j += item;
})
console.timeEnd('each')

这里显示一次运算的结果:

性能比较

从上图可以看出,for 循环的性能是明显好于 each 函数的,each 函数本质上也是用的 for 循环,到底是慢在了哪里呢?

我们再看一个例子:

function each(obj, callback) {
    var i = 0;
    var length = obj.length
    for (; i < length; i++) {
        value = callback(i, obj[i]);
    }
}

function eachWithCall(obj, callback) {
    var i = 0;
    var length = obj.length
    for (; i < length; i++) {
        value = callback.call(obj[i], i, obj[i]);
    }
}

var arr = Array.from({length: 1000000}, (v, i) => i);

console.time('each')
var i = 0;
each(arr, function(index, item){
    i += item;
})
console.timeEnd('each')


console.time('eachWithCall')
var j = 0;
eachWithCall(arr, function(index, item){
    j += item;
})
console.timeEnd('eachWithCall')

这里显示一次运算的结果:

性能比较

each 函数和 eachWithCall 函数唯一的区别就是 eachWithCall 调用了 call,从结果我们可以推测出,call 会导致性能损失,但也正是 call 的存在,我们才能将 this 指向循环中当前的元素。

有舍有得吧。

专题系列

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

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

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

@ArthurFree
Copy link

感谢作者分享哈!

但是因为 $("p").each() 方法是定义在 jQuery 函数的 prototype 对象上面的,而 $.data()方法是定义 jQuery 函数上面的,调用的时候不从复杂的 jQuery 对象上调用,速度快得多。所以我们推荐使用第一种写法。

这里是不是笔误了, $.data() 应该是 $.each

@mqyqingfeng
Copy link
Owner Author

@ArthurFree 哈哈,感谢指出~ o( ̄▽ ̄)d

@eva1963
Copy link

eva1963 commented May 7, 2018

刚看到博主的文章,正在学习阶段,非常受益,还有一个小问题想请教一下,就是each传递的第三个参数是什么情况,不太明白,
image

@cobish
Copy link

cobish commented Oct 22, 2018

@eva1963 第三个参数就是额外参数的意思了,你看下面使用到的 callback.apply

@linxh0908
Copy link

问题

这个地方试了下感觉下面两个没啥区别。。不知道是哪里没 get 到,郁闷

for (; i < length; i++) { if (callback.call(obj[i], i, obj[i]) === false) { break; } }
for (; i < length; i++) { callback.call(obj[i], i, obj[i]) ) }

有什么区别呢

$.each( [0, 1, 2, 3, 4, 5], function(i, n){
    // 这里判断也会 return,多个break 是为了什么?求解 
    if (i > 2) return false; 
    console.log( "Item #" + i + ": " + n );
 });

break是为了for循环不再向下执行了,跟callback没有关系了

@gNaW-tuanortsA
Copy link

gNaW-tuanortsA commented Mar 22, 2019

if (i > 2) return false;
i大于2后 出现return 后面console.log不再执行 但是each的for循环还在持续不过不再有输出
目的是i > 2后不再输出
所以用break直接跳出each的for循环 从而结束each 省去了后面的循环
@zouxiaomingya

@fangyinghua
Copy link

平时用forEach很少 我才知道forEach 不能跳出循环 以前没有注意

@domsgit
Copy link

domsgit commented Jul 21, 2019

纠正一个小细节:
`
var arr = Array.from({length: 1000000}, (v, i) => i);

console.time('for')
var i = 0;
for (; i < arr.length; i++) {
i += arr[i];
}
console.timeEnd('for')
`
这里循环内i跳步了,实际上没有循环1000000次。for用时大概在37ms左右,而不是0.04。

@blue1314
Copy link

blue1314 commented Aug 4, 2019

 function each(obj, callback) {
	    var i = 0;
	    var length = obj.length
	    for (; i < length; i++) {
	        value = callback(i, obj[i]);
	    }
	}
     
     function eachWithCall(obj, callback) {
	    var i = 0;
	    var length = obj.length
	    for (; i < length; i++) {
	        value = callback.call(obj[i], i, obj[i]);
	    }
	}
     
     function eachWithApply(obj, callback) {
     	var i = 0;
     	var length = obj.length;
     	for(; i < length; i ++) {
     		value = callback.apply(obj[i], [i,obj[i]]);
     	}
     }
     
     var arr = Array.from({length: 10000000}, (v, i) => i)
	 
	    console.time('each')
		var i = 0;
		each(arr, function(index, item){
		    i += item;
		})
		console.timeEnd('each')
		
		
		console.time('eachWithCall')
		var j = 0;
		eachWithCall(arr, function(index, item){
		    j += item;
		})
		console.timeEnd('eachWithCall')
		
		console.time('eachWidthApply')
		var i = 0;
		each(arr, function(index, item){
		    i += item;
		})
		console.timeEnd('eachWidthApply')
		
		//each: 815.879150390625ms
		//eachWithCall: 308.455078125ms
		//eachWidthApply: 828.31591796875ms

这里我的打印的each比eachWithCall慢???,博主能给我说说吗?

@angelayun
Copy link

angelayun commented May 17, 2020

@blue1314

 function each(obj, callback) {
	    var i = 0;
	    var length = obj.length
	    for (; i < length; i++) {
	        value = callback(i, obj[i]);
	    }
	}
     
     function eachWithCall(obj, callback) {
	    var i = 0;
	    var length = obj.length
	    for (; i < length; i++) {
	        value = callback.call(obj[i], i, obj[i]);
	    }
	}
     
     function eachWithApply(obj, callback) {
     	var i = 0;
     	var length = obj.length;
     	for(; i < length; i ++) {
     		value = callback.apply(obj[i], [i,obj[i]]);
     	}
     }
     
     var arr = Array.from({length: 10000000}, (v, i) => i)
	 
	    console.time('each')
		var i = 0;
		each(arr, function(index, item){
		    i += item;
		})
		console.timeEnd('each')
		
		
		console.time('eachWithCall')
		var j = 0;
		eachWithCall(arr, function(index, item){
		    j += item;
		})
		console.timeEnd('eachWithCall')
		
		console.time('eachWidthApply')
		var i = 0;
		each(arr, function(index, item){
		    i += item;
		})
		console.timeEnd('eachWidthApply')
		
		//each: 815.879150390625ms
		//eachWithCall: 308.455078125ms
		//eachWidthApply: 828.31591796875ms

这里我的打印的each比eachWithCall慢???,博主能给我说说吗?

image
我用博主一模一样的代码 大部分测试的结果跟博主的结果是一样的,但是也有个例,会出现call慢的情况
我觉得应该call没有性能问题,数据量都这么大的情况下,相关ms数也很小,说明没有明显的性能问题

@Vuact
Copy link

Vuact commented Dec 4, 2020

each方法,个人感觉这么写更酷一些:

function each(obj, callback) {
  const arraySw = isArrayLike(obj);
  for (let i in obj) {
    i = arraySw ? Number(i) : i;
    if (arraySw && isNaN(i)) {
      continue;
    }

    if (callback.call(obj[i], i, obj[i]) === false) {
      break;
    }
  }

  return obj;
}

@Chenmin926
Copy link

性能比较时, 普通for 循环和 each 函数的差异是否源于each函数会用多的执行上下文呢?

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