Advanced-Frontend/Daily-Interview-Question

第 46 题:输出以下代码执行的结果并解释为什么

Opened this issue · 48 comments

var obj = {
    '2': 3,
    '3': 4,
    'length': 2,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)

结果:[,,1,2], length为4
伪数组(ArrayLike)

留下了没技术的泪水

求解释.

以下为个人猜想没有确切的理论依据:

push() 方法将一个或多个元素添加到数组的末尾,并返回该数组的新长度。

根据MDN的说法理解,push方法应该是根据数组的length来根据参数给数组创建一个下标为length的属性,我们可以做以下测试:
image

根据这个测试我们发现,push方法影响了数组的length属性和对应下标的值。
然后,正如楼上所说:

在对象中加入splice属性方法,和length属性后。这个对象变成一个类数组。

我们使用题目中的代码时得到了这个结果:
image

这个时候控制台输出的是一个数组,但是实际上它是一个伪数组,并没有数组的其他属性和方法,我们可以通过这种方法验证:
image

所以我认为题目的解释应该是:

  1. 使用第一次push,obj对象的push方法设置 obj[2]=1;obj.length+=1
    2.使用第二次push,obj对象的push方法设置 obj[3]=2;obj.length+=1
    3.使用console.log输出的时候,因为obj具有 length 属性和 splice 方法,故将其作为数组进行打印
    4.打印时因为数组未设置下标为 0 1 处的值,故打印为empty,主动 obj[0] 获取为 undefined

第一第二步还可以具体解释为:因为每次push只传入了一个参数,所以 obj.length 的长度只增加了 1。push方法本身还可以增加更多参数,详见 MDN

我的理解是这样的
1: call push这个方法如果对象有length属性,length属性会加1 并且返回,这个是在某本书的上看到的,一直记得。
MDN

push方法将值追加到数组中。

push 方法有意具有通用性。该方法和 call() 或 apply() 一起使用时,可应用在类似数组的对象上。push 方法根据 length 属性来决定从哪里开始插入给定的值。如果 length 不能被转成一个数值,则插入的元素索引为 0,包括 length 不存在时。当 length 不存在时,将会创建它。

唯一的原生类数组(array-like)对象是 Strings,尽管如此,它们并不适用该方法,因为字符串是不可改变的。

  1. 调用push方法的时候会在调用对象的key=length的地方做一个赋值,不管前面key有没有值,也就是说在调用push的时候 对象实际被理解为了[0:undefined,1:undefined,2:3,3:4],
    这样也就有了结果里面的
    key===2 value =1
    key===3 value =2
    3.额外的
    这个对象如果有push和splice会输出会转换为数组,下图为去掉splice
    image
    包含splice方法
    image

关于push上面的同学解释的挺清楚的了,
我就想知道为什么对象添加了splice属性后会变成类数组对象?

留下了没技术的眼泪,求大佬出详细答案。

push 是特意设计为通用的,我们可以使用它来获得便利。正如下面的例子所示,Array.prototype.push 可以在一个对象上工作。 注意,我们没有创建一个数组来存储对象的集合。 相反,我们将该集合存储在对象本身上,并使用在 Array.prototype.push 上使用的 call 来调用该方法,使其认为我们正在处理数组,而它只是像平常一样运作。
var obj = {
length: 0,

addElem: function addElem (elem) {
    // obj.length is automatically incremented 
    // every time an element is added.
    [].push.call(this, elem);
}

};

// Let's add some empty objects just to illustrate.
obj.addElem({});
obj.addElem({});
console.log(obj.length);
// → 2

因为obj具有 length 属性和 splice 方法,故将其作为数组进行打印

@Moriarty02 同学讲的 push 很棒,不过关于这句话 这个对象如果有 push 和 splice 会输出会转换为数组 我亲自试了一下,发现对象没有 push 只要有 lengthsplice 就会变为类数组。

image

@kangkai124 我这边试了一下,只要一个对象的 length 属性为数字,同时splice属性为函数时, 对象的函数输出结果就会变成 伪数组。

var obj1 = {
    length: 1,
    splice: function () {},
}; // Object [empty, splice: ƒ]

var obj2 = {
    length: '1',
    splice: function () {},
}; // {length: "1", splice: ƒ}

var obj3 = {
    length: 1,
    splice: {},
}; // {length: 1, splice: {…}}

array-list 拥有length属性 , 属性为数字,即为类数组对象。 后面的 push等属性 是让类数组对象拥有部分数组方法特性。

var obj = {
    '2': 3,
    '3': 4,
    'length': 2,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)

涉及知识点:

  • 类数组(ArrayLike):

一组数据,由数组来存,但是如果要对这组数据进行扩展,会影响到数组原型,ArrayLike的出现则提供了一个中间数据桥梁,ArrayLike有数组的特性, 但是对ArrayLike的扩展并不会影响到原生的数组。

  • push方法:

push 方法有意具有通用性。该方法和 call() 或 apply() 一起使用时,可应用在类似数组的对象上。push 方法根据 length 属性来决定从哪里开始插入给定的值。如果 length 不能被转成一个数值,则插入的元素索引为 0,包括 length 不存在时。当 length 不存在时,将会创建它。
唯一的原生类数组(array-like)对象是 Strings,尽管如此,它们并不适用该方法,因为字符串是不可改变的。

  • 对象转数组的方式:

Array.from()、splice()、concat()等
题分析
这个obj中定义了两个key值,分别为splice和push分别对应数组原型中的splice和push方法,因此这个obj可以调用数组中的push和splice方法,调用对象的push方法:push(1),因为此时obj中定义length为2,所以从数组中的第二项开始插入,也就是数组的第三项(下表为2的那一项),因为数组是从第0项开始的,这时已经定义了下标为2和3这两项,所以它会替换第三项也就是下标为2的值,第一次执行push完,此时key为2的属性值为1,同理:第二次执行push方法,key为3的属性值为2。此时的输出结果就是:
Object(4) [empty × 2, 1, 2, splice: ƒ, push: ƒ]---->
[
2: 1,
3: 2,
length: 4,
push: ƒ push(),
splice: ƒ splice()
]

因为只是定义了2和3两项,没有定义0和1这两项,所以前面会是empty。
如果讲这道题改为:

var obj = {
    '2': 3,
    '3': 4,
    'length': 0,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)

此时的打印结果就是:
Object(2) [1, 2, 2: 3, 3: 4, splice: ƒ, push: ƒ]---->
[
0: 1,
1: 2,
2: 3,
3: 4,
length: 2,
push: ƒ push(),
splice: ƒ splice()
]

原理:此时length长度设置为0,push方法从第0项开始插入,所以填充了第0项的empty
至于为什么对象添加了splice属性后并没有调用就会变成类数组对象这个问题,这是控制台中 DevTools 猜测类数组的一个方式:
https://github.com/ChromeDevTools/devtools-frontend/blob/master/front_end/event_listeners/EventListenersUtils.js#L330

devtools 判断类数组的方法:

    /**
     * @param {?Object} obj
     * @return {boolean}
     */
    function isArrayLike(obj) {
      if (!obj || typeof obj !== 'object')
        return false;
      try {
        if (typeof obj.splice === 'function') {
          const len = obj.length;
          return typeof len === 'number' && (len >>> 0 === len && (len > 0 || 1 / len > 0));
        }
      } catch (e) {
      }
      return false;
    }

判断的过程:

  1. 存在且是对象
  2. 对象上的splice 属性是函数类型
  3. 对象上有 length 属性且为正整数

看文档:
1:push 方法根据 length 属性来决定从哪里开始插入给定的值。
2:push 是特意设计为通用的,Array.prototype.push 可以在一个对象上工作。

解析:
原题 length = 2。所以当然从第三个开始push,而obj中index为2和3的都被占用了。自然会替换掉。

所以:很得到的答案很明显。

涨姿势了,数组的push 方法根据 length 属性来决定从哪里开始插入给定的值以及判断伪数组的方法:
function isArrayLike(obj) {
if (!obj || typeof obj !== 'object')
return false;
try {
if (typeof obj.splice === 'function') {
const len = obj.length;
return typeof len === 'number' && (len >>> 0 === len && (len > 0 || 1 / len > 0));
}
} catch (e) {
}
return false;
}

这一题考察的是伪数组: (以下献丑)

首先搞清楚这一题要搞清楚 push ,其实push的时候会首先查询数组(伪数组)的 length 属性,接着在数组的最后一个添加上新的元素即 arr[length] (数组从零开始),然后length 增加一。 在这一题中,首先 伪数组查到length 是 2 ,就会 直接在 2 这个下标(属性) 上push 1 , 而length 会增加 1 变成 3 ,接着重复这个过程。

题外话: 伪数组 没有 length 的时候默认是 0。

前端小白, 有错勿怪, 欢迎指正。

ciyoe commented

没记错的话,在不同环境下会产生不同结果,
chrome下的console在判定数据类型事比较粗糙,slice与length同时存在时,判定为Array
node下的console会判定是 Object

看了一下运行结果后,有点懵逼,然后去翻了一下v8里array.js的实现代码。

function ArrayPush() {
  // 这个代码应该是c++实现的,作用应该是检查当前对象能不能转化成数组对象
  CHECK_OBJECT_COERCIBLE(this, "Array.prototype.push");
    
   if (%IsObserved(this))
     return ObservedArrayPush.apply(this, arguments);
    
  var array = TO_OBJECT_INLINE(this); 
  var n = TO_UINT32(array.length); // 获取array对象的length属性
  var m = %_ArgumentsLength(); // 获取参数的长度
   
  for (var i = 0; i < m; i++) {
    array[i+n] = %_Arguments(i); // 从下标为length开始push值
  }
   
    var new_length = n + m;
    array.length = new_length;
    return new_length;
  }

从实现的代码可以看出来,只要是对象拥有Array.prototype.push方法就会按照数组的push去执行。这句话说的直白点。

var obj = {
    '2': 3,
    '3': 4,
    'length': 2,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)

执行obj.push(1)时,当前length为2,正好替换了obj['2']的值,然后length变为3,obj.push(2)时就是替换obj['3']的值。就出来了浏览器的运行结果

var obj = {  
    '2': 3,
    '3': 4,
    'length': 2,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)

原题中
obj.length = 2 作为 splice方法的第一个参数,
当obj.push(1)时,相当于 ['', '', '3', '4'].splice(2, 1, 1)
当obj.push(2)时,相当于['', '', '3', '4'].splice(3, 1, 2)

obj = {
   '0': '',
   '1': '',
   '2': 1,
   '3': 2
}

假设将对象改成下面这样push两个数据
image
结果却是这样:
image
可以推测push的数据的索引应该是从对象length的值开始

所以当如题目这样
image
length值为2,所以push第一个数据时索引应从2开始,就覆盖了'2';
接着length长度加1,所以push的第二个数据索引为3,就覆盖了'3'。
结果:
image

以上为个人想法,欢迎各位讨论。

3N26 commented

function push (arr, item) {
arr[arr.length] = item;
}

image

length 2 从2 开始覆盖

难道这就是redux可时光倒流的原理吗?

留下了没技术的泪水

秀儿又是你

数组会根据length来进行push操作。例如以上的obj会根据length==2 来更新下标,也就是push会从2(length==2,意味着在[0,1,***]后添加)开始
然而下标值又是key值,所以会把key值为2,3的value替换成1,2

obj中length为指针 指向索引2

调用 obj.push(1) 等同于 obj[obj.length] = 1 length++
调用 obj.push(2) 等同于 obj[obj.length] = 2 length++

所以 变为 [,,1,2]

CC712 commented

规范
15.4.4.7 Array.prototype.push ( [ item1 [ , item2 [ , … ] ] ] )

The arguments are appended to the end of the array, in the order in which they appear. The new length of the array is returned as the result of the call.

When the push method is called with zero or more arguments item1, item2, etc., the following steps are taken:

  1. Let O be the result of calling ToObject passing the this value as the argument.
  2. Let lenVal be the result of calling the [[Get]] internal method of O with argument "length".
  3. Let n be ToUint32(lenVal).
  4. Let items be an internal List whose elements are, in left to right order, the arguments that were passed to this function invocation.
  5. Repeat, while items is not empty
  6. Remove the first element from items and let E be the value of the element.
  7. Call the [[Put]] internal method of O with arguments ToString(n), E, and true.
    Increase n by 1.

调用对象O 的 [[Put]] 内部方法,传参为 ToString(n) (键名),E(值),true,然后 n 增加 1

  1. Call the [[Put]] internal method of O with arguments "length", n, and true.
    Return n.
  2. The length property of the push method is 1.

NOTE The push function is intentionally generic; it does not require that its this value be an Array object. Therefore it can be transferred to other kinds of objects for use as a method. Whether the push function can be applied successfully to a host object is implementation-dependent.

第七步是原因

/** 
 * 1. 当一个对象拥有length属性,并且splice属性是个函数,对我们来说就可以看作是一个类数组
 * 2. 既然是类数组,对象的键就是数组的下标,对象的值就是数组当前下标的值
 * 3. 此时撇开length属性不管,这个类数组可以看作:[empty, empty, 3, 4]
 * 4. 当length属性起作用时,它将这个类数组的长度截断了,此时可以看作:[empty, empty]
 * 5. 之后这个类数组进行了两次push操作,结果可以看作:[empty, empty, 1, 2]
 * 6. 当然,这个类数组中还包含push和splice函数以及它的length,但并没有数组的其它方法,所以实 
 * 际上它只是一个对象而已
 */
let obj = {
    '2': 3,
    '3': 4,
    length: 2,
    splice: [].splice,
    push: [].push
};

obj.push(1);
obj.push(2);
console.log(obj); // [empty, empty, 1, 2]
console.log(Object.prototype.toString.call(obj)); // [object Object]

我感觉 学了个假的 js

MDN官网解释如下

var obj = {
    '2': 3,
    '3': 4,
    'length': 2,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)
var obj = {
    '2': 3,
    '3': 4,
    'length': 2,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}

存在length的值为number类型和splice值为函数类型,将obj变为伪数组;
根据length的值来创建数组长度,0,1下标无值所以实际上打印为
Object(2) [empty × 2, 2: 3, 3: 4, splice: ƒ, push: ƒ]
obj.push(1) //下标为2的值等于1, [empty × 2, 1, 3: 4, splice: ƒ, push: ƒ]
obj.push(2) //下标为3的值等于2, [empty × 2, 1, 2, splice: ƒ, push: ƒ]
同理变形一下

var obj = {
    '0': 3,
    '1': 4,
    'length': 3,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)  //Object(5) [3, 4, empty, 1, 2, splice: ƒ, push: ƒ]

以下为个人猜想没有确切的理论依据:

push() 方法将一个或多个元素添加到数组的末尾,并返回该数组的新长度。

根据MDN的说法理解,push方法应该是根据数组的length来根据参数给数组创建一个下标为length的属性,我们可以做以下测试:
image

根据这个测试我们发现,push方法影响了数组的length属性和对应下标的值。
然后,正如楼上所说:

在对象中加入splice属性方法,和length属性后。这个对象变成一个类数组。

我们使用题目中的代码时得到了这个结果:
image

这个时候控制台输出的是一个数组,但是实际上它是一个伪数组,并没有数组的其他属性和方法,我们可以通过这种方法验证:
image

所以我认为题目的解释应该是:

  1. 使用第一次push,obj对象的push方法设置 obj[2]=1;obj.length+=1
    2.使用第二次push,obj对象的push方法设置 obj[3]=2;obj.length+=1
    3.使用console.log输出的时候,因为obj具有 length 属性和 splice 方法,故将其作为数组进行打印
    4.打印时因为数组未设置下标为 0 1 处的值,故打印为empty,主动 obj[0] 获取为 undefined

第一第二步还可以具体解释为:因为每次push只传入了一个参数,所以 obj.length 的长度只增加了 1。push方法本身还可以增加更多参数,详见 MDN

那为什么一开始就有lengthsplice,但是打印出来是个对象呢?

var obj = {
  '2': 3,
  '3': 4,
  length: 2,
  splice: Array.prototype.splice,
  push: Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)

数组对象

JavaScript 数组的有一些特性是其他对象所没有的:

  • 当有新的元素添加到列表中的时候,自动更新 length 属性

  • 设置 length 为一个较小值的时候将会截断数组

  • Array.prototype 中继承一些有用的方法

  • 其类属性为 Array

这些特性让 JavaScript 数组和常规对象有明显的区别。但是它们不是定义数组的本质特性。

类数组对象

把拥有一个数值 length 属性和对应非负整数属性的对象看作一种类型的数组

我们定义的 function 函数中 Arguments 对象就是一个类数组对象,同样在客户端 JavaScript 中,一些 DOM 的方法比如 document.getElementsByTagName() 也是返回的类数组对象

// 判断是否是类数组对象
const isArrayLike = obj =>
  obj && // 非 null undefined
  typeof obj === 'object' && // 是对象
  isFinite(obj.length) && // 是有穷数
  obj.length >= 0 && // 为非负数
  obj.length === Math.floor(obj.length) && // 整数
  obj.length < Math.pow(2, 32) // < 4294967296

同时 JavaScript 数组方法是特意定义为通用的,因此它们不仅应用在真正的数组而且在类数组对象上都能正确的工作。类数组对象没有继承自 Array.prototype,不能直接直接调用数组的方法,但是也是可以通过 Function.call 方法调用。

那么我们改变一下题目

var obj2 = {
  '2': 3,
  '3': 4,
  length: 2
}

Array.prototype.push.call(obj2, 1)
Array.prototype.push.call(obj2, 2)
// 这样的到的有效结果是一样的  {2: 1, 3: 2, length: 4}

// 通过数组的方法 // 1 2
Array.prototype.forEach.call(obj2, item => console.log(item))
// 其本质还是一个类数组

结果

这是题目在 Chrome 浏览器控制台输出结果

Chrome控制台输出题目结果

我们改变的题目输出结果

改变的题目控制台输出题目结果

我们可以看到有效结果是一样的,那么为什么结果会是如此呢?

push() 方法将一个或多个元素添加到数组的末尾,并返回该数组的新长度。 push 方法根据 length 属性来决定从哪里开始插入给定的值。如果 length 不能被转成一个数值,则插入的元素索引为 0,包括 length 不存在时。当 length 不存在时,将会创建它。MDN

so ~

// obj.push(1)
// 等同于
obj['2'] = 1
obj.length++ // length = 3
// obj.push(2)
// 等同于
obj['3'] = 2
obj.length++ // length = 4
// 那么之后的结果就是我们看到的了

但是还是有不一样的地方比如

[empty × 2, 1, 2, splice: ƒ, push: ƒ]{2: 1, 3: 2, length: 4}

那么我们接下来继续看

继续看比较

只有当对象 splice 属性是一个 Function 的时候输出才为 [empty × 2, 1, 2, splice: ƒ, push: ƒ]

那么为此我又去 Firefox 控制台下面试了一下,结果如下图:

火狐控制台尝试

跟 Chrome 没有定义 splice 为 Function 是一致的

所以说可能是 Chrome 对其做的优化吧。

@kangkai124 我这边试了一下,只要一个对象的 length 属性为数字,同时splice属性为函数时, 对象的函数输出结果就会变成 伪数组。

var obj1 = {
    length: 1,
    splice: function () {},
}; // Object [empty, splice: ƒ]

var obj2 = {
    length: '1',
    splice: function () {},
}; // {length: "1", splice: ƒ}

var obj3 = {
    length: 1,
    splice: {},
}; // {length: 1, splice: {…}}

不够准确,length应该为自然数,负整数是不行的

yayxs commented

01-输出以下代码执行的结果并解释为什么

var obj = {
    '2': 3,
    '3': 4,
    'length': 2,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)

类(伪)数组(arraylike)

  • 就是像数组的对象(某些对象看起来像但不是)

  • 通过索引属性访问元素

  • 拥有 length 属性的对象

  • underscore 中的定义

    var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
    var getLength = property('length');
    var isArrayLike = function(collection) {
      var length = getLength(collection);
      return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX; // 其中 JavaScript 中能精确表示的最大数字
    };
  • 没有数组的方法(push forEach)

arrayLike.push('sex') // 01.js:20 Uncaught TypeError: arrayLike.push is not a function

形式

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

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

间接调用

Array.prototype.slice.call(arrayLike, 0); // ["name", "age", "sex"] 

转为真正的数组

Array.from(arrayLike); 

数组的push

push 方法具有通用性。该方法和 call()apply() 一起使用时,可应用在类似数组的对象上。push 方法根据 length 属性来决定从哪里开始插入给定的值。如果 length 不能被转成一个数值,则插入的元素索引为 0,包括 length 不存在时。当 length 不存在时,将会创建它。

唯一的原生类数组(array-like)对象是 Strings,尽管如此,它们并不适用该方法,因为字符串是不可改变的。

大白话

其实push的时候会首先查询数组(伪数组)的 length 属性,接着在数组的最后一个添加上新的元素即 arr[length]

var testObj = {
  "2": 3,
  "3": 4,
  length: 2,
  push: Array.prototype.push,
};


testObj.push(1) 
console.log(testObj) //// {2: 1, 3: 4, length: 3, push: ƒ}
testObj.push(2)
console.log(testObj) //{2: 1, 3: 2, length: 4, push: ƒ}
  • 第一点就是每次 push 后 length 会加1

00.png

'splice': Array.prototype.splice

 /**
     * @param {?Object} obj
     * @return {boolean}
     */
    function isArrayLike(obj) {
      if (!obj || typeof obj !== 'object') {
        return false;
      }
      try {
        if (typeof obj.splice === 'function') {
          const len = obj.length;
          return typeof len === 'number' && (len >>> 0 === len && (len > 0 || 1 / len > 0));
        }
      } catch (e) {
      }
      return false;
    }

为什么对象添加了splice属性后并没有调用就会变成类数组对象这个问题,这是控制台中 DevTools 猜测类数组的一个方式

  • 存在且是对象
  • 对象上的splice 属性是函数类型
  • 对象上有 length 属性且为正整数

楼上同学都答过了,我说一下个人理解,obj是一个类数组,本质是一个对象,但是拥有了数组的push和slice方法;而push方法其实就是在obj[length] 附上一个值,然而让length++这么一个操作;

JQSC commented

调用Array的push方法,累加对象length

var obj = {
    length: 2
}
Array.prototype.push.call(obj,1)
Array.prototype.push.call(obj,2)

image

给对象加上Array的splice方法变成伪数组

obj.splice= Array.prototype.splice
image

第 46 题:输出以下代码执行的结果并解释为什么

var obj = {
    '2': 3,
    '3': 4,
    'length': 2,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)

没有obj.push操作,直接输出obj可以看到,
image
即一个带有length和splice属性的对象会被浏览器解析成类数组。
然后当进行push时,因为obj的length为2,所以push会对第三位进行push,而obj的第三位已经有值了为3,所以会被替换为1,最终结果即为:
Object(4) [empty × 2, 1, 2, splice: ƒ, push: ƒ]

解这道题需要去了解Array.prototype.push 在V8下如何实现的

这是V8的push实现代码:

function ArrayPush () {
var n = TO_UNIT32(this.length);
var m = %_ArgumentsLength();
for (var i = 0; i < m; i++) { // 逐个复制元素
this[i + n ] = %_Arguments(i);
}
this.length = n + m; // 修改数组的length
return this.length;
}

通过代码我们知道会先获取原数组长度 n, 然后开始从下标n循环赋值:this[i+n]。this.length重新赋值为原数组长度加新push参数的长度,所以就能理解 {2:1, 3:2,length:4}是怎么来的了。
image

empty x 2则是因为下标从2开始,0和1都是空的,和数组一样如图:

image

同时应该也能明白 push(...[])为什么能push多个的原理了

只能说涨姿势了

var obj = {
    '2': 3,
    '3': 4,
    'length': 2,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)

node环境中运行结果:

{
  '2': 1,
  '3': 2,
  length: 4,
  splice: [Function: splice],
  push: [Function: push]
}

使用浏览器运行代码
Object(4) [empty × 2, 1, 2, splice: ƒ, push: ƒ]

  1. 元素变化
    因为push()是按照对象设置的length属性来判断追加位置,而不是根据最大键值。
    如此题第一次push(1)相当于obj[length] = 1,
    而不是obj[4]=1;
    添加元素后length++;
    补充:数组的length属性
    在JavaScript中数组的length是动态的,为最大键值数加一
let arr = [];
arr[1000] = 1;
console.log(arr.length); // 1001

数组的length属性是可写的,如果人为将length设置的小于当前数组内元素数目,元素数目会自动减少到length设置的大小。

let arr = [1,2,3];
arr.length = 0;
console.log(arr)// []
  1. 使用浏览器运行代码
    Object(4) [empty × 2, 1, 2, splice: ƒ, push: ƒ]
    obj被转换为类数组对象。而在node环境打印,依然是对象
    在Chrome Devtools中测试
obj1 = {
  splice: Array.prototype.splice,
};
// {splice: ƒ}
obj2 = {
  2: 1,
  splice: Array.prototype.splice,
};
// {2: 1, splice: ƒ}
obj3 = {
  length: 1,
  splice: Array.prototype.splice,
};
// Object [empty, splice: ƒ]
obj4 = {
  length: 2,
  splice: Array.prototype.splice,
};
// Object(2) [empty × 2, splice: ƒ]
obj5 = {
  length: 2,
  splice: () => {},
};
// Object(2) [empty × 2, splice: ƒ]
obj6 = {
  length: 2,
  splice: 3,
};
// {length: 2, splice: 3}

对象有length属性和splice函数, splice可以是自定义函数,在Chrome Devtools中就会被判定为类数组对象。

这题在我看来是两方面知识的理解:

  1. 鸭式辨型:像鸭子一样走路、游泳和嘎嘎叫的鸟就是鸭子。
  2. push:push方法是根据length属性来决定从哪里开始插入给定的值。

知道了上面这两点之后先来看obj

  • obj像数组一样,如果访问obj[2]将会得到3,访问obj.length会得到2,并且我们还赋给了这个对象push方法。

再来看push方法:

  • 如果我们更改数组的长度,那么push方法就会在更改的长度之后进行压入,并且将长度+1

    var arr = [1, 2, 3];
    arr.length = 4;
    arr.push(4);
    console.log(arr);
    //(5)[1,2,3,empty,4]

所以题解也就清晰了:

var obj = {
    '2': 3,
    '3': 4,
    'length': 2,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)

因为指定了类数组的length,所以push会根据length来压入,所以会在0,1这两个位置之后进行压入,此时2位置的3被更改为1。

之后push将改变length,原先的'length':2+1变成3,此时再压入2。

至于splice,这是让我不解的地方,经测试如果类数组同时拥有length和splice方法时,就会展示成数组的样式,但是本身不是数组。

var obj = {
  length: 2,
  splice: Array.prototype.splice,
};
console.log(obj);//Object(2) [empty × 2, splice: ƒ]
console.log(Array.isArray(obj));//false

对于Microsoft Edge和chrome他们会将其展示成数组的形式,而对于node和Firefox则会使用{}包起来。

看了一下运行结果后,有点懵逼,然后去翻了一下v8里array.js的实现代码。

function ArrayPush() {
  // 这个代码应该是c++实现的,作用应该是检查当前对象能不能转化成数组对象
  CHECK_OBJECT_COERCIBLE(this, "Array.prototype.push");
    
   if (%IsObserved(this))
     return ObservedArrayPush.apply(this, arguments);
    
  var array = TO_OBJECT_INLINE(this); 
  var n = TO_UINT32(array.length); // 获取array对象的length属性
  var m = %_ArgumentsLength(); // 获取参数的长度
   
  for (var i = 0; i < m; i++) {
    array[i+n] = %_Arguments(i); // 从下标为length开始push值
  }
   
    var new_length = n + m;
    array.length = new_length;
    return new_length;
  }

从实现的代码可以看出来,只要是对象拥有Array.prototype.push方法就会按照数组的push去执行。这句话说的直白点。

var obj = {
    '2': 3,
    '3': 4,
    'length': 2,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)

执行obj.push(1)时,当前length为2,正好替换了obj['2']的值,然后length变为3,obj.push(2)时就是替换obj['3']的值。就出来了浏览器的运行结果

想問如何讀 V8 code

感觉类似这个Array.prototype.push.call(obj, 1,2)

不得不说,面试是面试,工作是工作。如果代码审查时看到谁写这么难以理解的东西,直接两巴掌

这种题是哪些天才想出来的?哈哈

push 设计为通用方法
push 会 插入到 length 位置,如果 length 为正整数的话,否则插入到 0 的位置

splice 和 length 是 isArrayLike 的判断条件,使其认为为伪数组

https://github.com/ChromeDevTools/devtools-frontend/blob/04d4b64221e472bcbd5d1de16bef59c2cb9f8d02/front_end/panels/event_listeners/EventListenersUtils.ts#L286

在对象中加入splice属性方法,和length属性后。这个对象变成一个类数组。

我将这个代码直接在Chrome浏览器控制台运行,输出结果依然是对象,不是数组啊