教你简单用实现MVVM响应式原理(上) - Object.defineProperty的用法

271 阅读7分钟

MVVM的设计模式,相信很多人早就从别的文章中看到过了,这里就不过多叙述,直接上干货。

需要的基础知识:

  • Object.defineProperty (接下来我会先介绍这个属性的用法)

我们的最终目标:

  • 实现数据双向数据绑定功能: v-model
  • 点击事件的监听: v-on="clickd" (即@click)

首先来介绍一下Object.defineProperty的属性:

Object.defineProperty(obj, prop, descriptor)
// 参数介绍
// obj:要定义属性的对象
// prop: 要定义或修改属性的名称
// descriptor:对参数2(prop)的描述,也叫属性描述符

接下来重点说一下参数3(descriptor)属性描述符的用法

Object.defineProperty(对象, 属性名,{
    configurable:false, // 可配置
    enumerable:false,   // 可枚举
    
    writable:false,     // 可写入
    value:undefined,    // 初始值
    
    get:function(){}, // 重点 
    set:function(){}  // 重点
    
})
  • configurable: 为 true 时,属性才能重新被定义(再写一次Object.defineProperty)。默认为 false

  • enumerable:为true时,该属性才能够出现在对象的枚举属性中,即可以使用for in循环访问。默认为 false

  • value:该属性对应的初值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined

  • writable:为true时,value属性值才能被修改。默认为 false,相当是只读的。

  • get:一个给属性提供 get的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行。默认为 undefined

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

学习属性描述符(有基础的同学可以直接跳过)

enumerable :可枚举的

// 对象
var obj = {
    b:1
}
// 添加新属性
Object.defineProperty(obj,"a",{
    value:100, // 值
    enumerable:false // 不可枚举。不能被for in循环出来
})

Object.defineProperty(obj,"c",{
    value:200, // 值
    enumerable:true  // 可枚举。能被for in循环出来
})

console.dir(obj)


// 循环取出属性名。它只能取出 可枚举的属性名
// for(var 属性名 in 对象)
// 功能是:循环取出对象中的,可枚举的,所有属性名
for(var key in obj) {
    console.log(key)
}

结论:

enumerable只有为true,它才能被枚举(被for in循环)。

for of是循环属性值,for in 是循环属性名

configurable

可配置的。如果是false,则不能再次使用defineProperty去修改,修改就会报错。

也可以理解这个属性是只读的它不能被修改了。

var obj = {}
// 定义属性
Object.defineProperty(obj,"c",{

    configurable: false, 
    // 如果这个值是false,说明这属性将不能再次使用defineProperty来修改
    // 如果再通过:obj.c = 200,则也不会生效。
    value:100 
})
// 对现有属性的修改
// Uncaught TypeError: Cannot redefine property: c
// Object.defineProperty(obj,"c",{
//     value:200 
// })

console.log(obj)

value和writable

value:是属性初值

writeable: 可写入的

定义只读的属性:

const obj = {
    B:1
}
Object.defineProperty(obj,"A",{
    value: 1,
    writable: false
})

console.dir(obj)
console.log(obj.A)
obj.A =1001
console.log(obj.A)

const定义的对象,它的属性还是可以修改的。我们可以通过writable设置为false来设置只读的属性,真正实现常量的效果。

在上面的代码中,如果给对象的属性赋值,并不会修改属性的值。

进阶:对已有对象进行封装,以得到一个常量对象

目标:写一个函数,传入对象1,返回对象2。要求对象2的属性都不能被修改。

const obj = {
    a:1,
    b:2
}
function getConst(obj){
    var _obj = {}
    for(var key in obj){
        Object.defineProperty(_obj,key,{
            writable:false,
            value: obj[key]
        })
    }
    return _obj
}
var obj1 = getConst(obj)

console.log("设置之后的值是:",obj1)

get和set

get是一个方法,当访问对象的属性时,它会执行;

set是一个方法;当设置对象的属性时,它会执行;

  • 它们与value和writable是互斥的(一个属性的属性描述符中, 不能同时有 value和writable g与 get和set)。
  • 一旦使用它们,则这个属性就没有保存属性值的能力,如果希望它能保存属性性,则需要引入另一个额外的变量。
  • 应用:它们来做拦截器
var obj = {
        a:1
    }
    // 
    // Uncaught TypeError: Invalid property descriptor. 
    //  Cannot both specify accessors and a value or writable attribute

    // set get 不能与value和writeable同时存在 。


    // get,set 无法保存属性的值,只能借助另一个额外的变量

    // 需求,在你设置age属性时,如果属性>30岁,则统一改成28。
    var _age = 18
    Object.defineProperty(obj,'age',{
        get:function(){
            // obj.age ,则会执行get函数;
            // get()的返回值,就是obj.age的值
            // console.log('获取age属性')
            return _age
        },
        set(val){
            // obj.age = XX ,则会执行set函数,并且会传入值给val
            if(val > 30 ){
                console.log('程序员,30岁是一个劫')
                val = 28
            }
            _age = val
        }
    })

    console.log(obj);
    obj.age = 31;
    console.log(obj.age); // 28

下面我们做几道题巩固一下吧:

第1题

题目

下面代码会输出什么?

var obj = {a:1};
Object.defineProperty(obj,"a",{
    get(){
        return this.a
    }
})
console.log(obj.a)

友情请示: 如果在浏览器中运行上面的代码,可能会导致浏览器卡死。慎重。

答案

会陷入死循环。因为是上面的Object.definedProperty()给obj对象的a属性添加一个拦截器get(),当访问obj.a时,会执行get()中的代码,而在get()中又再次访问obj.a(this.a也就是obj.a),所以这里会形成死循环。

第2题

题目

Object.defineProperty()是否能在ie8中使用?

答案

不能。在这里可以查

第3题

题目

属性描述符一共有几个?

答案

6个。分别是configurable,enumable, value,writable,get,set;

第4题

题目

如何让一个对象属性处于只读状态。例如:

var obj = {a:1}
// 你的代码。
obj.a = 20;
console.log(obj.a) // ==> 1

答案

var obj = {a:1}
// 你的代码
Object.defineProperty(obj,'a',{
    writable:false
})
obj.a = 20;
console.log(obj.a) // ==> 1

第5题

题目

get,set能和value一起使用吗?

答案

不能。

第6题

题目

如何理解get,set是拦截器?

答案

当给一个属性定义了get,set之后,就说明它不能直接用来保存具体的数据了,数据只能保存在另一个变量中。此时我们就说这个属性是那个变量的拦截器。

如下的代码中:

var obj = {}
var _a = 1
Object.defineProperty(obj,"a",{
    set(val){ _a = val},
    get(){ return _a;}
})

obj.a就是_a的拦截器,获取和设置_a的两个操作都必须要经过obj.a的set,get函数。

第6题

题目

Object.defineProperty与vue框架有什么关系?

答案

vue.js 2.X版本中使用它来实现数据响应式效果。

第6题

题目

如何使用proxy来实现一个允许下标为负的数组。

答案

var arr = [1,2,3];
var proxyArr = new Proxy(arr,{
  get: (target,prop)=>{
    let index = Number(prop);
    if(index < 0){
      prop = target.length + index;
    }
    return target[prop];
  }
})

console.info(arr[-1]);   // undefined

console.info(proxyArr[-1]); // 3

第9题

题目

如何用proxy来实现访问不存在的属性名时给出更加优雅的提示。

const con = {
 COMPANYNAME:"jd",
}
// 你的代码
//let proxyConst = ...;//

console.log(proxyConst.abc); // 提示abc属性不存在。

答案

const con = {
 COMPANYNAME:"jd",
}

let proxyConst = new Proxy(con, {
  get: function (target, key, receiver) {
    if(key in target)
    return target[key];
    else{
      throw new Error("error:常量名"+key+"不存在!")
    }
  }

});

第10题

题目

如下代码会输出什么?

var obj = {
    a : 1
}
Object.defineProperty(obj,"b",{
    value: 2
})
obj.a = 100;
obj.b = 100;
console.log(obj.a, obj.b) // a,b的值分别是什么?

答案

obj.a是100,obj.b是2。因为writable属性默认为false,所以obj.b是不能修改了,或者说改了也无效。

第10题

题目

Object.defineProperty和Proxy的相同和不同?

答案

Object.defineProperty是es5中给出的,用来精细设置对象的属性的工具:例如可以通过writeable,enumable,configurable,set,get来设置只读属性,或者对某个属性进行拦截。 在vue2.x中就是使用它实现的数据响应式效果。

Proxy是es6提出新对象。它用来在一个已有对象的基础上设置代理对象。这个代理对象的作用是对原对象上的所有操作进行拦截,也可以充当拦截器的功能。

不同的之处在于,proxy的代理功能更加强大:

它可以很方便地拦截所有的属性操作;

除了get,set之外还有其它的代理功能。

在vue3中将会使用它来实现数据响应式效果。