初识JavaScript对象,数据劫持/数据代理

  • JavaScript对象语法、类型、属性
  • 属性描述符(getOwnPropertyDescriptor()、defineProperty())
  • [[Get]]、[[Put]]、Getter、Setter
  • 有必要了解Ojbect原型上的那些方法

 一、JavaScript对象语法、类型、属性、方法

 1.1对象字面量

复制代码
1 var obj = {
2     id:10,
3     value:"心上诗",
4     foo:function(){
5         console.log("播放歌曲" + this.value);
6     }
7 }
复制代码

1.2对象原型构造:

复制代码
1 var obj = new Object();
2 obj.id = 10;
3 obj.value = "心上诗";
4 obj.foo = function(){
5     console.log("播放歌曲"+this.value);
6 }
复制代码

1.3内置对象:

JavaScript有一些对象子类型,通常被称为内置对象:String、Number、Boolean、Object、Function、Array、Data、RegExp、Error;

null并不是对象,typeof null返回“object”只是语言的bug。检查对象类型详细了解:https://www.cnblogs.com/ZheOneAndOnly/p/10486016.html

这些内置对象很像其他语言的中类型(type)或者类(class),比如Java中的String类,但实际上在JavaScript中这些内置对象并不具备完整意义类的设计模式和功能,在接下来的两篇博客还会解析JavaScript的对象原型和class相关内容,以及new机制创建相应类型的对象和constructor方法,后面的博客都会有详细的解析。这里只关注对象本身。

1.4对象的内容(属性):

对象的内容是由一系列属性组成的,这些属性被存储在对象栈内存中“堆内存地址”所指向的特定位置,并呈key/val(键值对)的数据形式。而这并不是最终的形式,对象容器内部只是存储着这些属性的名称,它们就像指针一样(从技术角度来说就是应用),指向这些值的真正位置。

对属性最常见的操作就是访问调用属性,访问属性有两种方式:属性访问、键访问。

1 var obj = {
2     a:2
3 }
4 obj.a;//属性访问:2
5 obj["a"];//键访问:2

这两种访问属性的语法主要区别就在于(.)操作符要求属性满足标识符的命名规范,而([“...”])语法可以接收任意UTF-8/Unicode字符串作为属性名。如果由属性名是“Super-Fun”的属性,那就必须使用["Super-Fun"]语法访问。因为Super-Fun不是有效标识符属性名。

再就是["..."]语法可以使用字符串来访问属性,所以在程序中可以构造这个字符串,比如下面这个示例:

复制代码
1 var obj = {
2     a:2,
3     b:"333"
4 }
5 var idx = wantA ? "a" : "b";
6 obj[idx];
复制代码

在ES6中还新增了可计算属性名,详细可以了解这篇博客第二节:https://www.cnblogs.com/ZheOneAndOnly/p/11349574.html

关于JavaScript属性和方法在技术称谓上有些争议,一些比较严谨的说法是当对象调用方法时称为“函数引用”,当对象添加方法时称为“属性指向函数”。这是因为在JavaScript中函数不会永远属于一个对象,call,apply,bind这种改变函数执行的this指向就可以说明这一点。而在其他的一些语言中,方法的意思就是某类和对象上私有的,永远只能给自身或者类包含的对象调用的函数。

数组也是对象,所以虽然每个下标都是整数,但仍然可以给数组添加属性。有时候可能会将数组当作作对象使用,并不添加任何索引,但是这可能并不是什么好的主意,数组和普通对象都根据其对应的行为和用途进行了优化,所以最好只用对象存键值对,只用数组存下标/值对。

1.5复制对象

浅层复制(采用对象枚举的方式就可以实现):

复制代码
 1 var obj = {
 2     a:10,
 3     b:function(){console.log(this.a)}, //10
 4     c:{
 5         ca:"100",
 6         cb:function(){
 7             console.log(this.ca); //100
 8         }
 9     },
10     d:[1,"jsj",function(){console.log(this)}] //[1,"jsj",fun..]
11 };
12 var o = {};
13 for(var key in obj){ //基于枚举的浅复制
14     o[key] = obj[key];
15 }
复制代码

在编程中,更多的会关注深复制,也通常被称为深度克隆,详细实现方式可以了解我之前写的这篇博客:https://www.cnblogs.com/ZheOneAndOnly/p/9865001.html

上面的深度克隆采用的是递归算法实现的,资源消耗比较大,如果确定在对象中不会出现方法的话,可以考虑使用JSON的API来实现深度克隆:

//使用JSON的stringify()和parse()来实现没有方法的对象复制
var newobj = JSON.parse( JSON.stringify(someObj) );

 二、属性描述符

ES5之前,JavaScript语言本身没有提供可以直接检测属性特性的方法,比如判断属性是否只读。在ES5中Object上提供getOwnPropertyDescriptor(obj,attr)来查看属性的特性:

复制代码
1 var obj = {a:2};
2 Object.getOwnPropertyDescriptor(obj,"a"); //查看对象obj属性“a”的属性描述符
3 //返回结果
4 // {
5 //     value: 2,             //属性值
6 //     writable: true,      //可写(是否可以修改属性的值)
7 //     enumerable: true,     //可枚举(是否可以枚举这个属性)
8 //     configurable: true    //可配置(是否可以使用defineProperty(..)方法来修改属性描述符)
9 // }
复制代码

ES5中,除了提供查看属性特性的Object.getOwnPropertyDescriptor方法以外,还提供了一个配置属性特性的方法Object.defineProperty(),此方法可以直接在对象上顶一个新的具有详细描述的属性,或者修改一个对象的现有属性,并返回这个对象。

(通过查看对象特性以后,印证了前面对象内容描述的:对象存储区间内并不包含属性值,而是属性名,它们指向这些属性真正存储的物理地址,因为对象属性实际上是一个引用值类型的对象)

2.1writable:决定是否可以修改属性的值

复制代码
1 var obj = {};
2 Object.defineProperty(obj,"a",{
3     value:2,
4     wiritable:false
5 });
6 obj.a = 3; //在严格模式下修改不可修改的属性值会报错:
7 console.log(obj.a); //2
复制代码

2.2Configurable:只要属性是可配置的,就可以使用defineProperty()方法来修改属性描述符。但是要注意configurable一旦修改为false就无法在修改成true了。

复制代码
 1 var obj = {
 2     a : 2
 3 }
 4 Object.defineProperty(obj,"a",{
 5     value:3,
 6     writable:true,
 7     configurable:false,
 8     enumerable:true
 9 });
10 console.log(obj.a);//3
11 obj.a = 4;
12 console.log(obj.a);//4
13 Object.defineProperty(obj,'a',{ //这行会报错,这次配置全部不会生效
14     value:2,
15     writable:true,
16     configurable:true,
17     enumerable:true
18 });
复制代码

configurable:false除了禁止配置这个属性,还会禁止删除这个属性。

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

2.3Enumerable:决定属性是否可以枚举for(var key in Object)。

复制代码
 1 var obj = {};
 2 Object.defineProperty(obj,"a",{
 3     value:2,
 4     enumerable:true
 5 });
 6 Object.defineProperty(obj,"b",{
 7     value:3,
 8     enumerable:false
 9 });
10 for(var key in obj){
11     console.log(obj[key]); //2
12 }
复制代码

2.4属性不变性:

在前面了解到了属性的值并不是直接存在对象的内存区间内,在对象区间内存储的属性名称,指向属性真正的内存地址。然后通过获取属性描述符又了解到每个属性的真正值得表达是一个引用值对象,这个对象内包含了value值,writable是否可写,configurable是否可配置,enumerable是否可枚举,这些描述整体得描述了属性得状态。到了这里又引发了一个继续思考的问题,属性描述符真的可以控制属性的行为状态吗?比如属性值是数组、对象呢?这种引用值类型的值本身自己就是对象,它们的属性值会受到属性的描述符的控制吗?看下面这个示例:

复制代码
 1 var obj = {};
 2 Object.defineProperty(obj,"a",{
 3     value:[1,2,3],
 4     writable:false, //不可写
 5     configurable:true,
 6     enumerable:true
 7 });
 8 Object.defineProperty(obj,"b",{
 9     value:[1,2,3],
10     writable:false,    //不可写
11     configurable:false, //不可配置
12     enumerable:false
13 });
14 obj.a.push(4);
15 obj.b.push(4);
16 console.log(obj.a);    //[1, 2, 3, 4]
17 console.log(obj.b);    //[1, 2, 3, 4]
复制代码

通过示例,可以看到属性描述符实际上控制的是属性描述符value的栈内存,如果value是一个引用值类型,属性描述符并不能控制value指向的引用值的属性。也就是说,一个对象的属性的值并不能由属性的描述符完全控制,准确的说属性描述符不能控制对象的引用值类型的属性值。

虽然属性描述符完全控制属性,但是后面会有解决的办法,毕竟如果采用层级深度控制的话消耗性能,还不利于程序设计的灵活性,毕竟引用值共享赋值是有利于减低代码冗余的,并且还可以实现数据共享。

除了上面的defineProperty可以用来设置配置属性描述符,JavaScript还提供了一系列API来配置固定模式的属性描述符。同时还增加了一个约束对象属性扩张的API:

2.4.1禁止扩展:Object.prevent(obj)

1 var obj = {a:2};
2 Object.preventExtensions(obj); //禁止obj扩展属性
3 obj.b = 20;
4 console.log(obj.b); //undefined

2.4.2密封:Object.seal(obj)

密封对象实际上就是在禁止对象扩展的基础上,给属性设置了不可配置描述符:configurable:false

实际上密封实现的是,对象不能扩展属性,且不能删除属性;但还是可以通过(.)的方式修改属性的值,只是不能使用defineProperty来配置属性的值。

复制代码
1 var obj = {a:2};
2 Object.seal(obj); //密封
3 obj.b = 20;
4 console.log(obj.a,obj.b); //2  undefined --证明不能扩展
5 delete obj.a;
6 console.log(obj.a);//2 --证明不能删除
7 obj.a = 50;
8 console.log(obj.a);//50 --证明依然可以通过(.)修改属性值
复制代码

2.4.3冻结:Object.freeze(obj)

冻结对象实际上是在密封的基础上,给属性设置了不可写描述符:writable:false

实际上冻结对象就是,对象不能扩展,不能删除属性,不能重写属性。

复制代码
1 var obj = {a:2};
2 Object.freeze(obj); //冻结
3 obj.b = 20;
4 console.log(obj.a,obj.b); //2  undefined --证明不可扩展
5 delete obj.a;
6 console.log(obj.a);//2  --证明不可删除  
7 obj.a = 50;
8 console.log(obj.a);//2  --证明不可重写
复制代码

注:不论是密封还是冻结,本质上都是配置属性描述符,所以依然不能完全控制引用值类型的属性值的行为。

 三、[[Get]]、[[Put]]、Getter、Setter

 在解析四个关键词之前,先来看一个示例:

复制代码
 1 var obj = {a:2};
 2 console.log(Object.getOwnPropertyDescriptor(obj,"a"));//下面注释为打印结果
 3 //{
 4 //    value: 2, 
 5 //    writable: true, 
 6 //    enumerable: true,
 7 //    configurable: true
 8 //}
 9 Object.defineProperty(obj,"a",{
10     get:function(){
11         return 20;
12     }
13 });
14 console.log(obj.a);//20
15 console.log(Object.getOwnPropertyDescriptor(obj,"a"));//下面注释为打印结果
16 //{
17 //    get: ƒ, 
18 //    set: undefined, 
19 //    enumerable: true, 
20 //    configurable: true
21 //}
复制代码

在上面的示例中,给obj对象属性a的属性描述符配置get方法以后,属性a的属性描述符中的value(值)属性消失,writable(是否可写)属性消失。取而代之的是两个方法get和set,也就是说实际上对象属性的操作实际上受两种操作方式控制,当不配置属性的get和set方法时,属性默认[[Get]]、[[Put]]基于属性描述符的操作。当配置属性的get方法和set方法时,属性基于get()、set()方法的内部处理进行操作。

3.1[[Get]]、[[Put]]

到这里有可能会对[[Get]]、[[Put]]产生疑问,这两并没有出现在属性描述符中,那请思考下面这段代码:

1 var obj = {
2     a:2
3 }
4 console.log(obj.a);//2

基于前面的了解得知,属性值无论时原始值类型,还是引用值类型,它本身都是以一个属性描述对象的方式被属性名引用,如果按照正常的对象属性值调用逻辑应该是obj.a.value的方式获取属性值,但是实际上的获取方式却是obj.a,这就不难想到在这个环节隐式的调用了一个获取属性值的方法,这时候我们回想到obj.a.valueOf()方法,是的,这个方法显式的描述了属性值的实际获取方式,而这个方法再根据默认[[Get]]或者配置的get()方法来决定如何获取值。

详细的描述了[[Get]],那写入值得[[Put]]自然也就明白了,这里就不再赘述了。

3.2get()、set()

通过get()、set()配置写入和读出,get()依然受configurable的配置控制和enumerable枚举控制,而默认模式下的writable写入控制被set()方法替换。这也是在对象API中只有禁止扩展(preventExtensions)、密封(seal)、冻结(freeze),却没有控制写入的API,因为这些API依然对get()、set()配置的属性起作用。

复制代码
 1 var obj = {};
 2 Object.defineProperty(obj,"a",{
 3     set:function(){ //通过set配置默认值
 4         return 20;
 5     },
 6     get:function(){
 7         return 20; 
 8     },
 9     configurable:false, //不能配置
10     enumerable:false    //不能枚举
11 });
12 for(var key in obj){
13     console.log(key,obj[key]); //不能枚举到属性a
14 }
15 Object.defineProperty(obj,"a",{ //报错:不能配置
16     configurable:true
17 });
复制代码

3.3基于set和get方法配置属性的简写方式:

可以通过set()和get()直接在对象字面量中配置属性,而并不一定要defienProperty()来实现:

复制代码
 1 var obj = {
 2     set a(val){
 3         this._a_ = val * 2;
 4     },
 5     get a(){
 6         return this._a_;
 7     }
 8 }
 9 obj.a = 20;
10 console.log(obj.a);     //40
11 console.log(obj._a_);    //40
复制代码

至于为什么要使用obj._a_这种方式,这只是一种惯例,没有特殊行为,这种写法可以直接在控制台中查看对象属性时就了解到,ojb.a是通过set()、get()方式配置的属性。当然如果你不想这么写的话,直接在这两个方法中写return也可以,这里让我想起了ES6中的const声明常量,其底层就是基于直接的return来实现的,看下面的代码:

复制代码
 1 //可以基于set和get实现不可修改的属性
 2 var obj = {
 3     set a(val){ //当然可以不写set方法
 4         return 20;
 5     },
 6     get a(){
 7         return 20;
 8     }
 9 }
10 obj.a = 10;
11 console.log(obj.a);//20
复制代码

关于set()和get()方法有种应用叫做数据劫持,这个应用技术被广泛应用于各大js框架中,后面有博客解析这个应用。

 四、有必要了解Ojbect原型上的那些方法

 本来想把这块内容放到对象原型的相关博客中,但是这些方法的应用基本上与原型链的底层原理没有多大关系。

 

4.1.hasOwnProperty():判断对象上是否有指定的属性,不能判断原型链上的属性,返回boolean值。

1 var obj = {a:10};
2 console.log(obj.hasOwnProperty("a"));//true

4.2.propertyIsEnumerable():判断对象是否有指定的属性,并且是可枚举的属性,返回boolean值。

复制代码
 1 var obj = {};
 2 Object.defineProperty(obj,"a",{
 3     value:10,
 4     wiritable:true,
 5     configurable:true,
 6     enumerable:true
 7 });
 8 Object.defineProperty(obj,"b",{
 9     value:10,
10     wiritable:true,
11     configurable:true,
12     enumerable:false //不可枚举
13 });
14 console.log(obj.propertyIsEnumerable("a"));//true
15 console.log(obj.propertyIsEnumerable("b"));//false
复制代码

4.3.Object对象上的API判断属性是否存在、原型上的属性判断:

除了原型链上提供的这两个方法用来判断属性是存在,Object对象上还提供了两个API:

  • Object.getOwnPropertyNames(obj):获取对象属性名,返回一个对象属性名的数组。(自身所有属性,原型链上的属性名不能获取)
  • Object.keys(obj):获取对象上可枚举的属性名,返回一个对象属性名的数组。(自身所有属性,原型链上的属性名不能获取)

虽然不能直接判断原型上的属性是否存在,但是还是可以通过一些方法来查看,但不建议这么做,请看示例:

复制代码
 1 var obj = {
 2     set a(val){
 3         this._a_ = val;
 4     },
 5     get a(){
 6         return this._a_;
 7     }
 8 };
 9 Object.defineProperty(obj,"b",{
10     value:10,
11     wiritable:true,
12     configurable:true,
13     enumerable:true
14 });
15 Object.defineProperty(obj,"c",{
16     value:10,
17     wiritable:true,
18     configurable:true,
19     enumerable:false //不可枚举
20 });
21 console.log(obj.hasOwnProperty("a"));//true
22 console.log(Object.getOwnPropertyNames(obj));//["a", "b", "c"]
23 console.log(Object.keys(obj));//["a","b"]
24 var o = Object.create(obj); //o自身为一个空对象,它的原型是obj
25 console.log(o.hasOwnProperty("a"));//false
26 console.log(o.__proto__.hasOwnProperty("a"));//true
复制代码

4.4.剩余的其他方法:

  • toLocaleString():用来将数组转换成字符串模式,包含“,”间隔符,Object类型使用返回“[object Object]”;
  • toString():用来将值转换成字符串类型,详细了解:类型和原生函数及类型转换(二:终结js类型判断)
  • valueOf():用来获取属性值,内部依据[[Get]]或者get()来实现,第三节有详细的解析。
  • 剩下的__……__方法是浏览器私有方法,不提供给编程使用,但是还是可以了解以下它们有什么作用:

 __defineGetter__和__defineSetter__用来读写属性的内置方法,在读写属性时被调用。

__lookupGetter__和__lookupSetter__用来读写自身的方法,在读写自身时调用。

 

 

【出处】:https://www.cnblogs.com/ZheOneAndOnly/p/11371294.html

=======================================================================================

JS中关于数据劫持

数据劫持

  • 原因:

    • 将来我们使用框架的时候(vue),框架目前都支持一个 "数据驱动视图"
    • 完成数据驱动视图 需要借助 数据劫持 帮助我们完成
  • 定义

    • 以原始数据为基础,对数据进行一分复刻
    • 复制出来的数据是不允许修改的,值从原始数据里面获取
  • 语法

    • Object.defineProperty('哪一个对象','属性','配置项')
    • 配置项:
      • value: 这个属性对应的值
      • writable: 该属性是否可以被重写,默认是 false 不允许被修改
      • enumerable: 该属性是否可以被枚举,默认是 false 不能被枚举到
      • get: 是一个函数,叫做 getter 获取器,可以决定当前属性的值,不能与 value 与 writable 同时出现
      • set: 是一个函数,叫做 setter 设置器,当你需要修改当前属性的时候,会触发该函数
  • 下面是完整的数据劫持代码

<div class="box"></div>
<div class="box2"></div>

//获取元素
const box = document.querySelector('.box')
const box2 = document.querySelector('.box2')

//原始对象
const obj = {}
obj.name = '张三'
obj.age = 18

//将 obj 的属性,劫持到这个对象内
const res = {}

Object.defineProperty(res,'age',{
    enumerable:true,
    get () {
        return obj.age
    },
    set (val) {
        box.innerHTML = `res年龄: ${val}`
        obj.age = val
    }
})
res.age = 999
console.log(res)
box2.innerHTML = `obj年龄:${obj.age}`

封装数据劫持

<div class="box"></div>
<div class="box2"></div>
<script>
    const box = document.querySelector('.box')
    const box2 = document.querySelector('.box2')

    //原始对象
    const obj = {}
    obj.name = '张三'
    obj.age = 18

    //封装数据劫持
    function observer(origin,callback) {
        //1.创建一个对象,将 origin 内部的属性劫持到了这个对象内
        const target = {}

        //2.劫持 origin 上的属性到 target 中
        for(let key in origin){
            Object.defineProperty(target,key,{
                enumerable: reue,
                get () {
                    return origin[key]
                },
                set (val) {
                    origin[key] = val
                    callback(target)
                }
            })
        }
        //99将劫持后的 target 返回出去
        return target
    }

    const newObj = observer(obj,fn)

    function fn(res){
        box.innerHTML = `年龄:${res.age}; 名字:${res.name}`
    }

    newObj.age = 999
    newObj.name = 'gg'

</script>

数据劫持升级

  • 语法: Object.defineProperties('哪个对象','配置项')
<h1 class="box1"></h1>
<h1 class="box2"></h1>
<script>
    //获取元素
    const box1 = document.querySelector('.box1')
    const box2 = document.querySelector('.box2')

    //原始对象
    const obj = {}
    obj.name = '张三'
    obj.age = 18

    //将数据劫持后的对象属性存放在 res 对象中
    const res = {}
    //基础版
    // Object.defineProperties(res,{
    //   age:{
    //     get () {
    //       return obj.age
    //     },
    //     set (val) {
    //       box2.innerHTML = `res 对象的age属性: ${val},name 属性: ${res.name}`
    //       obj.age = val
    //     }
    //   },
    //   name: {
    //     get () {
    //       return obj.name
    //     },
    //     set (val) {
    //       box2.innerHTML = `res 对象的age属性: ${res.age},name 属性: ${val}`
    //       obj.name = val
    //     }
    //   }
    // })
    // console.log(res)

    //利用循环优化代码量
    for(let key in obj) {
        Object.defineProperties(res,{
            [key]:{
                get (){
                    return obj[key]
                },
                set (val) {
                    obj[key] = val
                    box2.innerHTML = `res 对象的 age 属性: ${res.age},name 属性: ${res.name}`
                }
            }
        })
    }

    //首次打开页面,给页面做一个赋值
    box1.innerHTML = `obj 对象的age属性: ${obj.age},name 属性: ${obj.name}`
    box2.innerHTML = `res 对象的age属性: ${res.age},name 属性: ${res.name}`

    //首次渲染完毕页面后 更改两个对象的属性值
    obj.age = 666
    obj.name = '李四'

    res.age = 999
    res.name = '锅锅'

</script>

数据劫持升级(自己劫持自己)

//获取元素he原始对象还用上面的
for (let key in obj){
    Object.defineProperties(obj,{
        /**
         * 通常我们在处理"自己劫持自己"的时候,不会再对象的原属性上操作,而是复制出来一份一模一样的数据操作
         * 
         * 为了和原属性名相同,所以会在原本的属性名前加一个 下划线,用来区分
        */
        ['_' + key]: {
            value:obj[key],
            writable:true
        },
        [key]: {
            get() {
                return obj['_' + key]
            },
            set(val) {
                obj['_' + key] = val
                box.innetHTML = `obj 对象的 age 属性: ${obj.age},name 属性: ${obj.name}`
            }
        }
    })
}

//首次渲染完毕页面后 更改两个对象的属性值
obj.age = 666
obj.name = '李四'

res.age = 999
res.name = '锅锅'

数据代理

  • 是官方给的名字,有部分程序员还是习惯性地叫做 数据劫持
  • proxy 是 ES6以后官方推出的 是一个内置构造函数
const obj = {
    name: '张三',
    age: 18
}

//new Proxy 第一个参数:要代理的对象,  第二个参数:一些配置项,  最后会返回一个代理后的对象,我们需要一个变量去接收
const res = new Proxy(obj,{
    get (target,property){
        /**
         * 第一个形参target: 就是你要代理的这个对象,在当前案例中指的就是 obj
         * 第二个形参property:就是该对象内部的某一个属性,自动分配
        */
        return target[property]
    },
    set(target,property,val){
        target[property] = val
        console.log(`你现在想要修改 形参 target 的属性${property},修改的值为 ${val},除此之外你还可以做很多事`)
    }
})

//在代理完成后,给原始对象新加一个属性,此时代理对象依然能够访问到 (Proxy 独有的功能)
obj.abc = 'qwer'

res.age = 66
res.name = '锅锅'

 

出处:JS中关于数据劫持 - 掘金 (juejin.cn)

posted on 2023-08-25 14:58  jack_Meng  阅读(226)  评论(0编辑  收藏  举报

导航