Object.defineProperty数据劫持和剖析Array.prototype.push的实现原理

577 阅读3分钟

二话不说,直接上代码

var a = ?;
if (a == 1 && a == 2 && a == 3) {
    console.log('OK');
}

这道题考查的是对于"=="操作类型转换,首先我们先来看看"="都有哪些特性:

1.= 赋值:变量和值的关联

2.== 比较:如果左右两边的数据类型不一致,则默认转换为一致的(对象==字符串,把对象转换为字符串,剩下的情况(除了null/undefined)一般都是要转换为数字)

3.基本数据类型转换为数字,默认都是隐式调用Number()处理的

4.对象转为数字,需要先转换为字符串(先调用valueOf方法,获取其原始值(基本数据类型基于构造函数创建出来的对象,他们的原始值都是基本值),如果原始值不是基本类型值,则继续调用toString),然后再把字符串转换为数字

5.=== 绝对比较:类型和值都得相同,类型不一样,也不会默认去转(推荐)

那此时我们知道了"=="具有数据类型转换的功能,所以就有了第一种方案

 var a = {
    i: 0,
    // 给当前对象重写“私有”方法valueOf/toString,这样就不会在向原型上找内置的方法
    valueOf() {
        // this => a
        return ++this.i;
    }
};

对象可以转换,数组也是对象,所以第二种方案:

var a = [1, 2, 3];
// 让A的私有属性toString等于ARRAY原型上的shift(每次执行都是删除数组第一项,返回的结果是删除的那一项)
a.toString = a.shift;

重头戏来了!

数据劫持

数据劫持(劫持对象中的某个属性): 在每一次获取a值的时候,我们把它劫持到,返回我们需要的值

那我们首先看看数据劫持都有哪些特性

Object.defineProperty(window, 'a', {
    get() {
        // 在每一次获取window.a的时候被触发执行
        // 返回啥值,本次获取的值就是啥
    },
    set(val) {
        // 在每一次给window.a赋值的时候执行
    }
});

我们这个例子不需要给a赋值,所以只用get()方法就够了

var i = 0;
Object.defineProperty(window, 'a', {
    get() {
        return ++i;
    }
});

if (a == 1 && a == 2 && a == 3) {
    console.log('OK');//OK
}

例2:
let obj = {
    2: 3,
    3: 4,
    length: 2,
    push: Array.prototype.push
}
obj.push(1);
obj.push(2);
console.log(obj);

这里面用到了push这个方法,那数组中的push实现的原理是什么呢?

Array.prototype.push:向数组末尾追加新的内容
Array.prototype.push = function push(val) {
    // this -> 当前操作的实例 arr
    1.向数组(THIS)末尾追加新的内容【数组索引是连续的,所以新增加这一项的索引肯定在原始最大的索引上加1】
    // this[this.length] = val;
 
    2.原始数组(THIS)的长度在之前的基础上会自动加1
    // this.length++;
 
    3. 返回新增后数组的长度
};

let arr = [10, 20];
let res = arr.push(30);
// res=3 新增数组的长度

所以我们就知道了原题中obj里面的属性都是自己私有后加的

obj.push(1);

相当于是:

  1. obj[obj.length]=1 =>obj[2]=1
  2. obj.length++ =>obj.length=3
  3. return没用

{2:1,3:4,length:3,push:...}

同理

obj.push(2);

  1. obj[obj.length]=2 =>obj[3]=2
  2. obj.length++ =>obj.length=4

所以:

console.log(obj); //{2:1,3:2,length:4,push:...}