Skip to content
New issue

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

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

Already on GitHub? # to your account

JavaScript深入之类数组对象与arguments #14

Open
mqyqingfeng opened this issue May 8, 2017 · 51 comments
Open

JavaScript深入之类数组对象与arguments #14

mqyqingfeng opened this issue May 8, 2017 · 51 comments

Comments

@mqyqingfeng
Copy link
Owner

mqyqingfeng commented May 8, 2017

类数组对象

所谓的类数组对象:

拥有一个 length 属性和若干索引属性的对象

举个例子:

var array = ['name', 'age', 'sex'];

var arrayLike = {
    0: 'name',
    1: 'age',
    2: 'sex',
    length: 3
}

即便如此,为什么叫做类数组对象呢?

那让我们从读写、获取长度、遍历三个方面看看这两个对象。

读写

console.log(array[0]); // name
console.log(arrayLike[0]); // name

array[0] = 'new name';
arrayLike[0] = 'new name';

长度

console.log(array.length); // 3
console.log(arrayLike.length); // 3

遍历

for(var i = 0, len = array.length; i < len; i++) {
   ……
}
for(var i = 0, len = arrayLike.length; i < len; i++) {
    ……
}

是不是很像?

那类数组对象可以使用数组的方法吗?比如:

arrayLike.push('4');

然而上述代码会报错: arrayLike.push is not a function

所以终归还是类数组呐……

调用数组方法

如果类数组就是任性的想用数组的方法怎么办呢?

既然无法直接调用,我们可以用 Function.call 间接调用:

var arrayLike = {0: 'name', 1: 'age', 2: 'sex', length: 3 }

Array.prototype.join.call(arrayLike, '&'); // name&age&sex

Array.prototype.slice.call(arrayLike, 0); // ["name", "age", "sex"] 
// slice可以做到类数组转数组

Array.prototype.map.call(arrayLike, function(item){
    return item.toUpperCase();
}); 
// ["NAME", "AGE", "SEX"]

类数组转数组

在上面的例子中已经提到了一种类数组转数组的方法,再补充三个:

var arrayLike = {0: 'name', 1: 'age', 2: 'sex', length: 3 }
// 1. slice
Array.prototype.slice.call(arrayLike); // ["name", "age", "sex"] 
// 2. splice
Array.prototype.splice.call(arrayLike, 0); // ["name", "age", "sex"] 
// 3. ES6 Array.from
Array.from(arrayLike); // ["name", "age", "sex"] 
// 4. apply
Array.prototype.concat.apply([], arrayLike)

那么为什么会讲到类数组对象呢?以及类数组有什么应用吗?

要说到类数组对象,Arguments 对象就是一个类数组对象。在客户端 JavaScript 中,一些 DOM 方法(document.getElementsByTagName()等)也返回类数组对象。

Arguments对象

接下来重点讲讲 Arguments 对象。

Arguments 对象只定义在函数体中,包括了函数的参数和其他属性。在函数体中,arguments 指代该函数的 Arguments 对象。

举个例子:

function foo(name, age, sex) {
    console.log(arguments);
}

foo('name', 'age', 'sex')

打印结果如下:

arguments

我们可以看到除了类数组的索引属性和length属性之外,还有一个callee属性,接下来我们一个一个介绍。

length属性

Arguments对象的length属性,表示实参的长度,举个例子:

function foo(b, c, d){
    console.log("实参的长度为:" + arguments.length)
}

console.log("形参的长度为:" + foo.length)

foo(1)

// 形参的长度为:3
// 实参的长度为:1

callee属性

Arguments 对象的 callee 属性,通过它可以调用函数自身。

讲个闭包经典面试题使用 callee 的解决方法:

var data = [];

for (var i = 0; i < 3; i++) {
    (data[i] = function () {
       console.log(arguments.callee.i) 
    }).i = i;
}

data[0]();
data[1]();
data[2]();

// 0
// 1
// 2

接下来讲讲 arguments 对象的几个注意要点:

arguments 和对应参数的绑定

function foo(name, age, sex, hobbit) {

    console.log(name, arguments[0]); // name name

    // 改变形参
    name = 'new name';

    console.log(name, arguments[0]); // new name new name

    // 改变arguments
    arguments[1] = 'new age';

    console.log(age, arguments[1]); // new age new age

    // 测试未传入的是否会绑定
    console.log(sex); // undefined

    sex = 'new sex';

    console.log(sex, arguments[2]); // new sex undefined

    arguments[3] = 'new hobbit';

    console.log(hobbit, arguments[3]); // undefined new hobbit

}

foo('name', 'age')

传入的参数,实参和 arguments 的值会共享,当没有传入时,实参与 arguments 值不会共享

除此之外,以上是在非严格模式下,如果是在严格模式下,实参和 arguments 是不会共享的。

传递参数

将参数从一个函数传递到另一个函数

// 使用 apply 将 foo 的参数传递给 bar
function foo() {
    bar.apply(this, arguments);
}
function bar(a, b, c) {
   console.log(a, b, c);
}

foo(1, 2, 3)

强大的ES6

使用ES6的 ... 运算符,我们可以轻松转成数组。

function func(...arguments) {
    console.log(arguments); // [1, 2, 3]
}

func(1, 2, 3);

应用

arguments的应用其实很多,在下个系列,也就是 JavaScript 专题系列中,我们会在 jQuery 的 extend 实现、函数柯里化、递归等场景看见 arguments 的身影。这篇文章就不具体展开了。

如果要总结这些场景的话,暂时能想到的包括:

  1. 参数不定长
  2. 函数柯里化
  3. 递归调用
  4. 函数重载
    ...

欢迎留言回复。

下一篇文章

JavaScript深入之创建对象的多种方式以及优缺点

深入系列

JavaScript深入系列目录地址:https://github.com/mqyqingfeng/Blog

JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。

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

@eczn
Copy link

eczn commented May 8, 2017

image

感觉如果类数组对象的原型指向 Array.prototype 他可以被认为是一个数组了。


image

毕竟 typeof {} 跟 typeof [] 结果是一样的。

@mqyqingfeng
Copy link
Owner Author

mqyqingfeng commented May 8, 2017

@eczn 哈哈,把原型指向Array.prototype后就可以调用Array.prototype上的方法,行为上确实是跟数组一样,然而Array.isArray和Object.prototype.toString不认呐😂

default

@eczn
Copy link

eczn commented May 8, 2017

233 很接近 但是还是有所区别

@stoneyallen
Copy link

Array.prototype.concat.apply([], arguments)
这个是不是写错了,不应该是arguments

@mqyqingfeng
Copy link
Owner Author

@stoneyallen 十分感谢指出,确实是写错了。o( ̄▽ ̄)d

@hugeorange
Copy link

谢谢楼主的分享!我把您的文章里的 demo 全敲了一遍,有两个地方不太明白,还请指教!
md格式的不太会用写的有点丑陋,还请见谅

callee 属性 解决闭包经典面试题的那个例子,虽然跑通了,但不明白是什么意思?
这是什么写法,不太懂??
(data[i] = function () { console.log(arguments.callee.i) }).i = i;

传递参数里面,demo 没有跑通

`

     function foo(){

              bar.apply(this,arguments); 

             // 这句的意思是把 bar的参数 传递给 foo 吗? 如果是的话,下面会打印出 3 ,

             console.log(arguments.callee.length); // 0
     }

    function bar(a,b,c){
              console.log(arguments); // []
              console.log(arguments.callee.length); // 3
    }
    foo()
    bar()

`

还有楼主应该在补充讲一下,arguments还有一个属性 caller 指向 调用当前函数的函数的引用

@mqyqingfeng
Copy link
Owner Author

哈哈,那我把我的回复再回复一遍哈,如果以后有相同的问题,大家也都可以看到~

@mqyqingfeng
Copy link
Owner Author

关于第一个问题,写个简单例子:

var fun1 = function(){}

fun1.test = 'test';

console.log(fun1.test)

函数也是一种对象,我们可以通过这种方式给函数添加一个自定义的属性。
这个解决方式就是给 data[i] 这个函数添加一个自定义属性,这个属性值就是正确的 i 值。

@mqyqingfeng
Copy link
Owner Author

关于第二个问题,是把foo的参数传递给bar,可以看这个跑通的例子:

function foo() { bar.apply(this, arguments); }

function bar(a, b, c) { console.log(a, b, c) }

foo(1, 2, 3)

@mqyqingfeng
Copy link
Owner Author

关于caller,直接截图MDN哈:

default

@gnipbao
Copy link

gnipbao commented Jun 2, 2017

解释的很详细!!我再补充点

类数组检测

function isArrayLike(o) {
    if (o &&                                // o is not null, undefined, etc.
        typeof o === 'object' &&            // o is an object
        isFinite(o.length) &&               // o.length is a finite number
        o.length >= 0 &&                    // o.length is non-negative
        o.length===Math.floor(o.length) &&  // o.length is an integer
        o.length < 4294967296)              // o.length < 2^32
        return true;                        // Then o is array-like
    else
        return false;                       // Otherwise it is not
}

arguments

image
如图可以看出

  1. arguments的长度只与实参的个数有关,与形参定义的个数没有直接关系。
  2. arguments 有一个Symbol(Symbol.iterator)属性这个表示该对象是可迭代的

思考

image

  • 字符串可以像类数组一样操作是因为js自动包装成String对象的原因,String对象照上面检测函数也是类数组对象。不过因为本身值不能被改变,所以给指定下标赋值不会改变。

@mqyqingfeng
Copy link
Owner Author

@gnipbao 非常感谢补充!o( ̄▽ ̄)d

这个类数组对象的判断方法应该是来自《JavaScript权威指南》吧,很多库比如 underscore 和 jQuery 中也有对数组和类数组对象的判断,比如 jQuery 的实现:

function isArrayLike(obj) {

    // obj必须有length属性
    var length = !!obj && "length" in obj && obj.length;
    var typeRes = type(obj);

    // 排除掉函数和Window对象
    if (typeRes === "function" || isWindow(obj)) {
        return false;
    }

    return typeRes === "array" || length === 0 ||
        typeof length === "number" && length > 0 && (length - 1) in obj;
}

underscore 的实现:

  var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
  var isArrayLike = function(collection) {
    var length = collection.length;
    return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};

@dongliang1993
Copy link

var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
var isArrayLike = function(collection) {
var length = collection.length;
return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};
这种判断方式 array 也会返回 true

@mqyqingfeng
Copy link
Owner Author

@dongliang1993 确实是这样的, jQuery 和 underscore 的 isArrayLike 都是既判断数组又判断类数组对象的~

@dongliang1993
Copy link

这样的话,感觉 _.each 函数就有点问题了,
_.each = _.forEach = function(obj, iteratee, context) {
iteratee = optimizeCb(iteratee, context)
var i, length
if (isArrayLike(obj)) { // const obj = {a: 1, length: 1} 会直接进入到下面的循环,变成了obj[0],obj[1]
for (i = 0, length = obj.length; i < length; i++) {
iteratee(obj[i], i, obj)
}
} else {
const keys = _.keys(obj)
for (i = 0, length = keys.length; i < length; i++) {
iteratee(obj[keys[i]], keys[i], obj) // (value, key, obj)
}
}
return obj
};
其实应该是要用 for in 来遍历吧?

@mqyqingfeng
Copy link
Owner Author

@dongliang1993 没有问题呐,类数组对象是可以使用 for 循环遍历的呐~

@dongliang1993
Copy link

@mqyqingfeng 我的意思是,如果是 obj = { name: 'xiaoming', length: 1 } 这样的类数组对象,isArrayLike 判断为 true,然后进入相应的迭代器,用 for 循环是 iteratee(obj[i], i, obj) 这样的,可是 i 是 0, 1, 2...这样的数字,那 obj[0],obj[1] 都是 undefined呀,可是 obj 明明是有 'name' 这个属性的。不知道大佬有没有看明白我的意思。。。

@mqyqingfeng
Copy link
Owner Author

@dongliang1993 确实会出现这样的问题, { name: 'xiaoming', length: 1 } 可以通过 underscore 的 isArrayLike 验证,但是在 each 函数中,obj[0] 为 undefined。关键还是在于这个对象并不是一个严格意义上的类数组对象,isArrayLike 可以校验出我们开发中会用到的 arguments 对象,满足我们的开发需求,但是对于我们故意创造出的对象,确实也会漏掉~

@mqyqingfeng
Copy link
Owner Author

@dongliang1993 如果用 for in 遍历类数组对象的话,length 和 自定义的一些属性也会被遍历到,也会导致问题吧~

@huangmxsysu
Copy link

好像说在函数中传递arguments给任何参数,将导致Chrome和Node中使用的V8引擎跳过对其的优化,这也将使性能相当慢。
请问博主知道其中的原因么?

@huangmxsysu
Copy link

忘了在哪里看到的了

@mqyqingfeng
Copy link
Owner Author

mqyqingfeng commented Sep 5, 2017

@huangmxsysu 这个是来自 blueBird 的 wiki,https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments,以前也查过这个问题,之所以降低性能,是因为:

Leaking the arguments object kills optimization because it forces V8 to instantiate the arguments as a Javascript object instead of optimizing them into stack variables.

当时想不明白的是为什么 [].slice.call(arguments) 依然会导致性能损失,现在想想,可能是因为将 this 指向 arguments,所以依然保持了对 arguments 的引用吧

@mqyqingfeng
Copy link
Owner Author

其实本篇应该添加 leaking arguments 的部分,告诉大家不要乱用 arguments 😂

@huangmxsysu
Copy link

噢好像是因为这个原因哈!
是啊,昨天看到你数组去重那篇中有个_.union函数,就想着应该讲arguments转换一下,类似这样

function union() {
	//最好能把arguments转换一下
	var args = new Array(arguments.length);
	for(var i = 0; i < args.length; ++i) {
	    args[i] = arguments[i];
	}
    return unique(flatten(args, true, true));
}

@huangmxsysu
Copy link

flatten那篇

@ClarenceC
Copy link

这篇文章的内容比上那几章瞬间简单了很多啊😂,没有源码没有模拟.不过精彩的地方还是在评论区,很多学习的地方啊.

@mqyqingfeng
Copy link
Owner Author

@ClarenceC 有读者说看我的文章没有看懂,看评论看懂了😂

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng
Copy link
Owner Author

补充一点箭头函数和 arguments 相关的规范部分:

函数初始化的时候,如果是箭头函数,会设置内部属性 [[ThisMode]] 为 'lexical'

If kind is Arrow, set the [[ThisMode]] internal slot of F to lexical.

创建函数上下文的时候:

If the value of the [[ThisMode]] internal slot of func is lexical, then

NOTE Arrow functions never have an arguments objects.

Let argumentsObjectNeeded be false.

@HuangQiii
Copy link

_20180201220715

这里应该是数组,类数组本来就是对象~

@AngellinaZ
Copy link

(data[i] = function () { console.log(arguments.callee.i) }).i = i;

请问大大,arguments.callee.i是给函数添加i属性,那外围的(...).i = i 是什么意思

@mqyqingfeng
Copy link
Owner Author

@HuangQiii 感谢指出哈,这里写错了,应该是数组

@mqyqingfeng
Copy link
Owner Author

@AngellinaZ 其实是 (...).i = i 给函数添加了 i 属性,然后通过 arguments.callee.i 获取了这个属性值:

(data[i] = function () { console.log(arguments.callee.i) }).i = i;

就相当于:

data[i] = function () { console.log(arguments.callee.i)
data[i].i = i;

@EtheriousNatsu
Copy link

EtheriousNatsu commented Mar 21, 2018

var data = [];

for (var i = 0; i < 3; i++) {
    (data[i] = function () {
       console.log(arguments.callee.i) 
    }).i = i;
}

data[0]();
data[1]();
data[2]();

// 0
// 1
// 2

我想问下, 按照之前的文章,AO是在执行函数的时候才进行初始化,然后在函数执行的过程中改变AO。这行代码 data[0].i=0 执行的时候,还没有执行 data[0],所以这时候还没有进入函数执行上下文,那么i是怎么保存到 data[0] Context 的 AO中的?

@mqyqingfeng
Copy link
Owner Author

mqyqingfeng commented Mar 28, 2018

@EtheriousNatsu i 的值是存放在 data[i].i 中的,当执行 data[0]() 的时候,此时相当于:

var data = [
   {i: 0},
   {i: 1},
   {i: 2}
]

function() {
  console.log(data[0].i)
}

这行代码 data[0].i=0 执行的时候,虽然没有执行 data[0],但是 data[i] = function () { console.log(arguments.callee.i) } 已经执行了,i 的值就保存在这个函数对象中。

@tobeapro
Copy link

tobeapro commented Oct 31, 2019

var data = [];

for (var i = 0; i < 3; i++) {
    (data[i] = function () {
       console.log(arguments.callee.i) 
    }).i = i;
}

data[0]();
data[1]();
data[2]();
// 0
// 1
// 2

作者你好,你说的这里利用闭包,我没太理解,执行结果我是理解的。
上面循环完的结果就是下面这样

data[0] = function(){
  console.log(arguments.callee.i) 
}
data[0].i = 0;
data[1] = function(){
  console.log(arguments.callee.i) 
}
data[1].i = 1;
data[2] = function(){
  console.log(arguments.callee.i) 
}
data[2].i = 2;

所以

data[0](); //0
data[1](); //1
data[2](); //2

因为我理解的闭包就是在函数中声明了某个变量,然后在函数内部返回了一个子函数且子函数使用了这个变量;
😂然后上面的例子我感觉就是访问了一个(函数)对象的属性

--------分割线-------
是我看错了😂,没认真看标题
讲个闭包经典面试题使用 callee 的解决方法:,原来你说的意思是利用callee达到闭包的效果,并不是说利用闭包

@Lirong6
Copy link

Lirong6 commented Mar 22, 2020

大大,请问这里是不是应该是形参和arguments不会共享?arguments代表实参的值呀

传入的参数,实参和 arguments 的值会共享,当没有传入时,实参与 arguments 值不会共享

除此之外,以上是在非严格模式下,如果是在严格模式下,实参和 arguments 是不会共享的。

`
function foo(name, age, sex, hobbit) {
'use strict';
console.log(name, arguments[0]); // name name

// 改变形参
name = 'new name';

console.log(name, arguments[0]); // new name name

// 改变arguments
arguments[1] = 'new age';

console.log(age, arguments[1]); // age new age

// 测试未传入的是否会绑定
console.log(sex); // undefined

sex = 'new sex';

console.log(sex, arguments[2]); // new sex undefined

arguments[3] = 'new hobbit';

console.log(hobbit, arguments[3]); // undefined new hobbit

}

foo('name', 'age')
`

@HowToMeetYou
Copy link

sex = 'new se
x';

console.log(sex, arguments[2]); // new sex undefined

大大,请问这里是不是应该是形参和arguments不会共享?arguments代表实参的值呀

传入的参数,实参和 arguments 的值会共享,当没有传入时,实参与 arguments 值不会共享

除此之外,以上是在非严格模式下,如果是在严格模式下,实参和 arguments 是不会共享的。

`
function foo(name, age, sex, hobbit) {
'use strict';
console.log(name, arguments[0]); // name name

// 改变形参
name = 'new name';

console.log(name, arguments[0]); // new name name

// 改变arguments
arguments[1] = 'new age';

console.log(age, arguments[1]); // age new age

// 测试未传入的是否会绑定
console.log(sex); // undefined

sex = 'new sex';

console.log(sex, arguments[2]); // new sex undefined

arguments[3] = 'new hobbit';

console.log(hobbit, arguments[3]); // undefined new hobbit

}

foo('name', 'age')
`

当没有传入时,实参与 arguments 值不会共享
// 测试未传入的是否会绑定
console.log(sex); // undefined

sex = 'new sex';

console.log(sex, arguments[2]); // new sex undefined

@OldDream
Copy link

OldDream commented Apr 1, 2020

(data[i] = function () {
console.log(arguments.callee.i)
})
js 的赋值语句会返回值,比如上面就会返回
function () {
console.log(arguments.callee.i)
}

@anjina
Copy link

anjina commented Nov 22, 2020

`
function test(a, b, c = 10) {
console.log(arguments); // [1, 2]
console.log(a, b, c); // 1 2 10
arguments[0] = 2; // [2, 2]
console.log(a); // 1
a = 3; // 3
console.log(arguments); // [2, 2]
b = 3;
console.log(arguments[1]); // [2, 2]
c = 20;
console.log(arguments); // [2, 2]
}

test(1, 2);
`
目前浏览器和node环境测试表明 实参和arguments之间没有什么关联了。有人解释下吗

@anjina
Copy link

anjina commented Nov 22, 2020

`
function test(a, b, c = 10) {
console.log(arguments); // [1, 2]
console.log(a, b, c); // 1 2 10
arguments[0] = 2; // [2, 2]
console.log(a); // 1
a = 3; // 3
console.log(arguments); // [2, 2]
b = 3;
console.log(arguments[1]); // [2, 2]
c = 20;
console.log(arguments); // [2, 2]
}

test(1, 2);
`
目前浏览器和node环境测试表明 实参和arguments之间没有什么关联了。有人解释下吗

MDN找到答案了,是因为我使用了参数默认值。

在严格模式下,剩余参数、默认参数和解构赋值参数的存在不会改变 arguments对象的行为,但是在非严格模式下就有所不同了。

当非严格模式中的函数没有包含剩余参数、默认参数和解构赋值,那么arguments对象中的值会跟踪参数的值(反之亦然)

@lsc9
Copy link

lsc9 commented Dec 13, 2021

只有非严格模式下,且形参中没有rest参数、默认值和结构赋值时 arguments 才会与参数绑定。

@czy5997
Copy link

czy5997 commented Apr 4, 2022

function foo() { bar.apply(this,arguments) }
这个里面的this 是谁的this呀,为啥这样写

@xingqq
Copy link

xingqq commented Jan 17, 2025

arguments 和对应参数的绑定-部分

不应该是形参和arguments的值是共享的吗?
实参是函数调用时传递的值,给形参进行初始赋值,函数中name值的改变应该是形参的值吧

@loveheavenlina
Copy link

loveheavenlina commented Jan 17, 2025 via email

@rainbowyy
Copy link

rainbowyy commented Jan 17, 2025 via email

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

No branches or pull requests