es6的新老绑匪 —— proxy 和 Object.defineProperty()

前言

在 JavaScript 的世界里,数据的流动和交互是至关重要的。然而,直接操作数据可能会带来一系列的问题,例如数据污染、安全隐患等。于是,JavaScript 中的"绑匪"------Proxy 出场了。Proxy,顾名思义,就是一个代理人,它可以站在目标对象的前面,拦截和修改对该对象的访问和操作。通过 Proxy,我们可以实现数据绑定、权限控制、性能优化等一系列的功能。那么,Proxy 究竟是如何工作的?它又能带来什么样的便利和安全性?现在让我们抓住这个劫匪来对他严刑逼供一下。

1. 数据劫持

在我们对proxy进行审讯之前,先来了解一下它犯了什么事以及它的前身是什么。接下来我们先来看看它的前身,我们看到文章的题目就知道proxy的作用呢就是进行数据劫持,在proxy出现之前就有一个函数可以进行数据劫持,它就是Object.defineProperty()。在了解它之前我们先来看看什么是数据劫持?

数据劫持

  1. 劫持对象中的某一个属性,可以控制该属性是否可读可写可枚举可配置, 还可以指定该属性的值,以及当该属性值被读取时会触发 get 方法,当该 属性值被修改时会触发 set 方法。

  2. 只能接收对象,不能劫持数组

1.1 Object.defineProperty()

在了解了数据劫持的一些基本概念之后,我们来看看如何使用Object.defineProperty()来实现数据劫持。

首先我们先定义一个对象obj,其中有个属性a并且将其赋值成1,接下来我们要对obj中的a进行数据劫持,这时候就要使用Object.defineProperty()了,这个函数的参数我们可以传入三个分别是想要劫持的对象劫持对象的属性 和一个对象,下面我们来看代码展示:

js 复制代码
let obj = {
  a: 1
}

// 数据劫持
Object.defineProperty(obj, 'a', {
  value: 100// 让a的值变成100
})

obj.a = 2
console.log(obj.a);


// 输出:
// 2

这时候的输出结果是2,这里可能就有同学疑惑了,这明明还能给a修改数值,劫持又算怎么一回事呢?先别急,在Object.defineProperty()中的第三个参数对象中我们可以为其配置属性,而其中的writable就可以控制被劫持的属性是否可写,接下来我们在其中加入writable: false来看看效果:

js 复制代码
let obj = {
  a: 1
}

// 数据劫持
Object.defineProperty(obj, 'a', {
  value: 100,// 让a的值变成100
  writable: false // obj中的a是否可写
})

obj.a = 2
console.log(obj.a);

// 输出:
// 100

当我们添加了这个属性时候我们可以发现被劫持的obj中的a属性不能进行修改,只能由Object.defineProperty()中的value属性来修改劫持的数据,这就是我们所说的数据劫持。

简单理解呢就是我们可以使用Object.defineProperty()来绑架某一个数据,这时候只能由我们绑匪说了算,别人想被劫持的数据干啥也没用,因为人在我们手里,想撕票就撕票。

除了这两个属性之外,还有很多属性可以让我们对这个人质上点手段,比如configurable,这个属性可以让对象上面的属性不被移除,下面我们来看看代码展示:

js 复制代码
let obj = {
  a: 1
}

// 数据劫持
Object.defineProperty(obj, 'a', {
  configurable: true, // 是否可配置 === 是否可移除
})

delete obj.a
console.log(obj.a);

// 输出:
// undefined

当我们将configurable设置成true时就代表该属性可以被移除,但是设置成false的话就代表这个属性不可以被移除,接下来我们将其设置成false看看效果:

js 复制代码
let obj = {
  a: 1
}

// 数据劫持
Object.defineProperty(obj, 'a', {
  configurable: false, // 是否可配置 === 是否可移除
})

delete obj.a
console.log(obj.a);

// 输出:
// 1

我们可以看到此时obj中的a并没有被移除,那么它身上的第三个参数只能这样配置属性了吗?作为一名合格的绑匪应该有很多种对待人质的方法来充分展现它的价值,它身上除了能配置属性之外,还能配置两个方法分别是getset

1.1.1 get()

get():一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。默认为 undefined。

接下来呢我们先来聊聊 Object.defineProperty() 中的 get(),当我们使用这个方法的时候得注意一点:

Object.defineProperty(object, propertyName, descriptor) 定义新属性时,descriptor 中不能同时有 访问器(getter/setter) 与 value/writable 属性。

这时候有同学就会疑惑了,既然不能跟value和writable同时使用,那这个方法有什么用呢?这个方法不同于我们平常的方法需要我们手动进行调用,当我们想要访问它劫持的那个属性时,这个方法会自动调用并且返回你想要获取的那个属性,接下来我们来看看代码展示:

js 复制代码
let obj = {
  a: 1
}

// 数据劫持
Object.defineProperty(obj, 'a', {
  get() {
    return obj.a
  },
})

console.log(obj.a);

当我们运行的时候会发现,怎么爆栈了呢?我们前面明明说了获取a的时候会自动调用get()然后return一个我们想要拿到的obj.a,咋就爆了呢?

这里有一个小坑得注意一下,我们思路是没错的,但是当我们返回obj.a时,是不是又要去获取被劫持的a属性,然后就又要调用一次get(),当调用它的时候,然后又要重新获取obj.a。这样的话不就进入了无限套娃死循环吗,这时候就会出现爆栈这个问题了。

那么我们如何才能从绑匪手中拿到obj.a呢?这时候我们可以在绑匪绑架之前先来个偷梁换柱,设置一个变量value用来储存obj.a的值,然后在调用get方法的时候return value,接下来我们修改一下代码来试试效果:

js 复制代码
let obj = {
  a: 1
}
let value = obj.a

// 数据劫持
Object.defineProperty(obj, 'a', {
  get() {
    return value
  },
})

console.log(obj.a);

// 输出:
// 1

这时候我们发现可以正常输出了,但是有没有想过一个问题,我们如果在外面修改obj.a的话,那打印出来的obj.a是修改后的还是原来的值呢?下面我们来看看结果:

js 复制代码
let obj = {
  a: 1
}
let value = obj.a

// 数据劫持
Object.defineProperty(obj, 'a', {
  get() {
    return value
  },
})

obj.a = 2
console.log(obj.a);

// 输出:
// 1

这时候我们发现输出结果还是1,因为我们返回的还是那个value并没有改变过value的值,那这时候我们就会发现好像修改不了obj.a的值了,那咋办呢?这时候就需要set登场了。

1.1.2 set()

set():一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。默认为 undefined。

set()这个函数可以传入一个参数val,用来接收我们对劫持的数据修改后的数据,接下来我们来用一段代码展示一下:

js 复制代码
let obj = {
  a: 1
}
let value = obj.a

// 数据劫持
Object.defineProperty(obj, 'a', {
  get() {
    return value
  },
  set(val) {
    console.log('获取到了val', val);
  }
})

obj.a = 2
console.log(obj.a);

我们根据控制台的打印结果可以得知,当我们对劫持的数据进行修改的时候,set()就会被触发,并且传入其中的参数就是修改后的那个值。但是此时我们发现obj.a的值并没有随之修改,这个时候我们可以根据set每次修改被劫持数据就会自动触发一次时的特性从而修改value的值,进而改变我们想要拿到的obj.a的值,接下来我们来看代码:

js 复制代码
let obj = {
  a: 1
}
let value = obj.a

// 数据劫持
Object.defineProperty(obj, 'a', {
  get() {
    return value
  },
  set(val) {
    value = val
    console.log('获取到了val', val);
  }
})

obj.a = 2
console.log(obj.a);

这时候我们可以看到已经成功修改了obj.a的属性并且能够获取到。

小测验

在了解完了getset这两个函数的作用和用法之后,下面我们来实现一个小功能:现在我有一个对象obj,里面有三个属性分别是a、b、c并且都是Number类型,每次我对里面的任何一条数据进行修改时,都要给我打印修改后这三个属性的和,接下来我们来实现一下这个功能。

首先我们如果想要每次修改之后都计算一下总和,那么肯定要一个计算总和的函数,并且在每次修改之后都会执行一次,这里就得用到我们的set了,它就相当于一个监听器一样,每次修改劫持的数据时都会触发,有了这个思路之后,下面我们来看一下代码实现:

js 复制代码
let obj = {
  a: 1,
  b: 2,
  c: 3
}

for (let key in obj) {
  let value = obj[key]
  Object.defineProperty(obj, key, {
    get() {
      return value
    },
    set(val) {
      value = val
      log()
    }
  })
}

obj.a = 10
obj.b = 20
obj.c = 30

function log() {
  console.log(obj.a + obj.b + obj.c);
}

在上面代码中我们使用了一个for in来对obj中每个属性都进行数据劫持,并且各自都有一个value所以不会爆栈,根据set()的特性,我们只需要将每次修改的属性对应key然后将value赋值成新的值即可。这样每次进行修改的时候,obj中的对应属性值会被更新,并且函数log()也会执行一次。

1.1.3 手写watch

我们都知道vue中有几个可以进行监听的函数,常用的就有computed和watch,这类函数可以监听我们页面的数据变化,当数据变换的时候可以执行相应的操作。这里大家有没有联想到我们前面所讲的set(),它同样也有这个特性,每当劫持的数据发生变更时,就自动执行一次,利用这个特性我们可以手写一个watch,接下来我们来实现一下。

官方定义的watch是能传入几个参数的,在这里我们就按照Object.defineProperty()的特性给我们自定义的wwatch传入三个参数,分别是对象想要劫持的数据回调函数,然后我们对传入的数据进行劫持,并且在watch中设置一个value用来储存这个数据防止爆栈,接下来我们来看html和js代码以及页面展示:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <h1 id="count">1</h1>
    <button id="btn">add</button>
  </div>

  <script>
    const btn = document.getElementById('btn')
    const h1 = document.getElementById('count')

    let obj = {
      count: 1
    }

    watch(obj, 'count', (newVal, oldVal) => {
      console.log(newVal);
    })

    // 手写 watch
    function watch(obj, key, cb) {
      let value = obj[key]
      Object.defineProperty(obj, key, {
        get() {
          return value
        },
        set(newValue) {
          cb(newValue, value)
          value = newValue
        }
      })
    }

    btn.addEventListener('click', () => {
      obj.count++
    })

  </script>
</body>
</html>

2. proxy

在我们了解完了Object.defineProperty()也就是proxy的前身之后,会不会觉得这个东西写起来有点复杂还得注意value爆栈的问题,官方呢为了方便我们在新版本的es6中推出了proxy这个方法,毕竟时代变了咱得文明一点不能老是劫匪劫匪的,现在改名叫代理了。

proxy

  1. 直接代理整个对象,对象上的读取值、修改值、添加属性、删除属性等13个操作, 都会被代理到某个函数上

  2. 可以代理数组

接下来我们来看看proxy的使用方法,其实跟它的前身是差不多的,一样有get和set,就是把它们变成了属性,后面接收一个函数,下面我们来看代码示例:

js 复制代码
let obj = {
  a: 1,
  b: 2,
  c: 3
}


// 代理是整个对象都被绑架,简单来说就是对象上的种种行为都有一个代理函数
let proxy = new Proxy(obj, {
  get: function (target, key) {
    return target[key]
  },
  set: function (target, key, value) {
    target[key] = value
  },
  // xxx 包括set get 一共13个函数
})

proxy.a = 10// 会触发 set
console.log(proxy.a);// 会走proxy中的get方法


// 输出:
// 10

根据上面的代码我们可以看到不用向之前一样设置value用来接收数据,可以直接return对象中的某个属性了,并且可以直接代理整个对象,而不是某一个属性,除了上面的几种方法之外,还有很多新增的方法,具体的呢有兴趣的同学可以参考一下[阮一峰 ECMAScript 6 (ES6) 标准入门教程 第三版]

结语

在ES6中,Proxy就像一个机智的绑匪,可以根据不同的情况改变自己的行为和外貌。通过使用Proxy,我们可以更好地控制对象的访问和修改,实现数据的保护和验证。同时,Proxy也为我们提供了一个强大的工具,来实现一些复杂的功能,如数据绑定、缓存和日志记录等。因此,Proxy是ES6中一个非常值得学习和使用的特性,能够让我们的代码更加灵活、安全和高效。

谢谢各位大佬们的观看!!!

相关推荐
工业甲酰苯胺10 分钟前
Java Web学生自习管理系统
java·开发语言·前端
百里落云13 分钟前
2024年终总结,人生已过半,一半回忆,一般继续!
面试
网络安全-老纪1 小时前
[网络安全]DVWA之XSS(DOM)攻击姿势及解题详析合集
前端·web安全·xss
蓝天星空1 小时前
html生成注册与登录代码
javascript·css·html
宜昌李国勇1 小时前
`http_port_t
android·前端
我家猫叫佩奇2 小时前
React项目eslint8 升级到 9记录
前端
API_Zevin2 小时前
如何优化亚马逊广告以提高ROI?
大数据·开发语言·前端·后端·爬虫·python·学习
野槐2 小时前
CSS进阶和SASS
前端·less·scss
玩具工匠3 小时前
字玩FontPlayer开发笔记3 性能优化 大量canvas渲染卡顿问题
前端·javascript·vue.js·笔记·elementui·typescript
CodeClimb3 小时前
【华为OD-E卷 - 服务失效判断 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od