1 - Object.defineProperty()

81 阅读5分钟

知识点:

一、Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

  1. 语法:Object.defineProperty(obj, prop, descriptor)
  • obj:要定义属性的对象。
  • prop:要定义或修改的属性的名称或 Symbol 。
  • descriptor:要定义或修改的属性描述符。
  • 返回值:被传递给函数的对象。
  1. defineProperty相关:
  • 可以利用Object.defineProperty()给对象定义多个属性
  • 使用Object.defineProperty()默认的对它的属性进行了增加之后,所输出的对象,它不可修改、不可枚举、不可删除
  • 数据劫持:把对象里的属性进行可配置、可写、可枚举的一种配置,然后通过get()和set()方法对存、取值进行逻辑上的扩展;
  • 属性描述符主要有两种形式:数据描述符存取描述符;数据描述符特有的两个属性:value和writable;存取描述符特有的两个属性:get和set;两种形式的属性描述符不能混合使用;
  1. defineProperty缺点:
  • Object.defineProperty能够劫持对象的属性,但是需要对对象的每一个属性进行遍历劫持;如果对象上有新增的属性,则需要对新增的属性再次进行劫持;如果属性是对象,还需要深度遍历。这也是为什么Vue给对象新增属性需要通过$set的原因,其原理也是通过Object.defineProperty对新增的属性再次进行劫持。
  • Object.defineProperty在劫持对象和数组时的缺陷:
    1. 无法检测到对象属性的添加或删除
    2. 无法检测数组元素的变化,需要进行数组方法的重写
    3. 无法检测数组的长度的修改

二、Object.defineProperties() 方法直接在一个对象上定义新的属性或修改现有属性,并返回该对象。

  1. 语法:Object.defineProperties(obj, props)
  • obj:在其上定义或修改属性的对象。
  • props:要定义其可枚举属性或修改的属性描述符的对象。

一、小案例

1. 用Object.defineProperty()增加的属性不可修改、不可枚举、不可删除

function defineProperty() {
  let _obj = {};
  Object.defineProperty(_obj, 'a', {
    value: 1
  })
  return _obj;
}
let obj = defineProperty();
console.log('obj', obj); // {a:1}

//1.属性值不可修改
obj.a = 5;
console.log('obj', obj); //{a:1}

//2.属性也不可枚举
for (let k in obj) {
  console.log(k + ":" + obj[k]);
}

//3.属性不可删除
delete obj.a;
console.log('obj', obj); //{a:1}

2. Object.defineProperty在劫持对象和数组时的缺陷:

  • 无法检测到对象属性的添加或删除
  • 无法检测数组元素的变化,需要进行数组方法的重写
  • 无法检测数组的长度的修改
// 1.无法检测到对象属性的添加或删除
var list = [1, 2, 3];
list.map((elem, index) => {
    Object.defineProperty(list, index, {
        get: function () {
            console.log("get index:" + index);
            return elem;
        },
        set: function (val) {
            console.log("set index:" + index);
            elem = val;
        }
    });
});

// 往数组list中添加数据4,没有输出
list.push(4);
list[3] = 5;

为此,Vue的解决方案是劫持Array.property原型链上的7个函数,通过下面的函数简单进行劫持: 在数组上进行操作的push、shift等函数都是调用的原型对象上的函数,因此将改写后的原型对象重新给绑定到实例对象上的__proto__,这样就能进行劫持。

//2.无法检测数组元素的变化,需要进行数组方法的重写
const arratMethods = [
    "push",
    "pop",
    "shift",
    "unshift",
    "splice",
    "sort",
    "reverse",
];

const arrayProto = Object.create(Array.prototype);

arratMethods.forEach((method) => {
    const origin = Array.prototype[method];
    arrayProto[method] = function () {
        console.log("run method", method);
        return origin.apply(this, arguments);
    };
});

const list = [];

list.__proto__ = arrayProto;

//run method push
list.push(2);
//run method shift
list.shift(3);
// 3.无法检测数组的长度的修改
var list = [];
//通过给length修改为10,数组中有10个undefied,虽然我们给每个元素都劫持了,但是没有触发get/set函数。
list.length = 10;
list.map((elem, index) => {
    Object.defineProperty(list, index, {
        get: function () {
            console.log("get index:" + index);
            return elem;
        },
        set: function (val) {
            console.log("set index:" + index);
            elem = val;
        },
    });
});

list[5] = 4;
// undefined
console.log(list[6]);

3.Object.defineProperties() 方法直接在一个对象上定义新的属性或修改现有属性,并返回该对象。

function defineProperties() {
  let _obj = {};
  Object.defineProperties(_obj, {
    a: {
      value: 2,
      writable: true, //可修改
      enumerable: true, //可枚举
      configurable: true, //可操作、可配置
    },
    b: {
      value: 3
    }
  })
  return _obj;
}

// 1.属性值可修改
let obj1 = defineProperties();
obj1.a = 6; //属性值a可修改
obj1.b = 7;
console.log('obj1', obj1); // {a:6,b:3}

// 2.属性a可枚举
for (let k in obj1) {
  console.log(k + ":" + obj1[k]); // a:6
}

//3.属性可删除
delete obj1.a;
console.log('obj1', obj1); // {b:3}

4. value、writable中任意一个与get() 、set()中任意一个方法互斥

  • 属性描述符主要有两种形式:数据描述符存取描述符;数据描述符特有的两个属性:value和writable;存取描述符特有的两个属性:get和set;两种形式的属性描述符不能混合使用;
  • 两种描述不能混合使用;value用来定义属性的值,而get和set同样也是定义和修改属性的值,两种描述符在功能上有明显的相似性。
  • 虽然数据描述符和存取描述符不能混着用,但是他们均能分别和configrable、enumerable一起搭配使用,下面表格表示了两种描述符可以同时拥有的健值:

 function defineProperties1() {
   let _obj = {},
       a = 1;
   Object.defineProperties(_obj, {
     a: {
       //value: 1,
       //writable: true, //value、writable中任意一个与get() 、set()中任意一个方法互斥
       get() {
         return a;
       },
       set(newVal) {
         console.log('newVal', newVal)
       }
     },
     b: {
       value: 6
     }
   })
   return _obj;
 }
let obj2 = defineProperties1();
console.log(obj2);
obj2.a = 8;

5. Object.defineProperties() 要定义其可枚举属性

function DataArr() {
  let _val = null,
      _arr = [];
  Object.defineProperty(this, 'val', {
    get() {
      return _val;
    },
    set(newVal) {
      _val = newVal;
      _arr.push({
        val: _val
      });
      console.log('_val', _val);
    }
  })
  this.getArr = function () {
    return _arr;
  }
}
let dataArr = new DataArr();
dataArr.val = 123;
dataArr.val = 234;
console.log(dataArr.getArr()); //[{val:123},{val:234}]

二、计算器案例

效果:

1. html页面布局

<body>
    <div class="J_calculator">
        <div class="result"></div>
        <div><input type="text" class="f-input" /></div>
        <div><input type="text" class="s-input" /></div>
        <div class="btn-group">
            <button data-field="plus" class="current">+</button>
            <button data-field="minus">-</button>
            <button data-field="mul">*</button>
            <button data-field="div">/</button>
        </div>
    </div>
    <script src="../../js/utils.js"></script>
    <script src="./js/index.js"></script>
</body>

2. css样式

<style>
  .J_calculator {
    margin: 50px 100px;
  }

.result {
  font-size: 14px;
  margin-bottom: 10px;
  text-indent: 10px;
}

.J_calculator input {
  height: 28px;
  line-height: 28px;
  border: 1px solid #eee;
  text-indent: 10px;
  margin-bottom: 10px;
  outline: none;
  border-radius: 3px;
}

.btn-group button {
  height: 28px;
  line-height: 28px;
  width: 28px;
  border: 1px solid #eee;
  border-radius: 3px;
  outline: none;
}

.current {
  background: orange;
  color: #fff;
}
</style>

3.index.js

class Compute {
    plus(a, b) {
        return a + b;
    }
    minus(a, b) {
        return a - b;
    }
    mul(a, b) {
        return a * b;
    }
    div(a, b) {
        return a / b;
    }
}
//继承Compute类
class Calculator extends Compute {
    constructor(doc) {
        super();
        const oCal = doc.getElementsByClassName('J_calculator')[0]; 
        this.fInput = oCal.getElementsByTagName('input')[0];
        this.sInput = oCal.getElementsByTagName('input')[1];
        this.oBtnGroup = oCal.getElementsByClassName('btn-group')[0];
        this.oBtnItems = this.oBtnGroup.getElementsByTagName('button');
        this.oResult = oCal.getElementsByClassName('result')[0];
        this.data = this.defineData();
        this.btnIdx = 0;
    }
    init() {
        this.bindEvent();
    }
    bindEvent() {
        this.oBtnGroup.addEventListener('click', this.onFieldBtnClick.bind(this), false);
        this.fInput.addEventListener('input', this.onNumberInput.bind(this), false);
        this.sInput.addEventListener('input', this.onNumberInput.bind(this), false);
    }

    defineData() {
        let _obj = {},
            fNumber = 0,
            sNumber = 0,
            field = 'plus';
        const _self = this;
        Object.defineProperties(_obj, {
            fNumber: {
                get() {
                    return fNumber;
                },
                set(newVal) {
                    console.log('fNumber', newVal)
                    fNumber = newVal;
                    _self.computeResult(fNumber, sNumber, field);
                }
            },
            sNumber: {
                get() {
                    return sNumber;
                },
                set(newVal) {
                    console.log('sNumber', newVal)
                    sNumber = newVal;
                    _self.computeResult(fNumber, sNumber, field);
                }
            },
            field: {
                get() {
                    return field;
                },
                set(newVal) {
                    console.log('field', newVal)
                    field = newVal;
                    _self.computeResult(fNumber, sNumber, field);
                }

            }
        })
        return _obj;
    }
    computeResult(fNumber, sNumber, field) {
        console.log('field', field)
        this.oResult.innerText = this[field](fNumber, sNumber);
    }
    //点击 + - * / 按钮
    onFieldBtnClick(ev) {
        const e = ev || window.event,
            tar = e.target || e.srcElement,
            tagName = tar.tagName.toLowerCase();
        console.log(tagName)
        tagName === 'button' && this.fieldUpdate(tar); 
    }
    //获取当前点击的 + - * /按钮类型
    fieldUpdate(target) {
        this.oBtnItems[this.btnIdx].className = '';
        this.btnIdx = [].indexOf.call(this.oBtnItems, target);
        target.className += ' current';
        this.data.field = target.getAttribute('data-field');
    }

    onNumberInput(ev) {
        const e = ev || window.event,
            tar = e.target || window.srcElement,
            className = tar.className,
            //‘\s’表示正则匹配字符串中的空字符,‘g’表示全部匹配
            val = Number(tar.value.replace(/\s+/g, '')) || 0; //替换字符串中所有的空字符串
        switch (className) {
            case 'f-input':
                this.data.fNumber = val;
                break;
            case 's-input':
                this.data.sNumber = val;
                break;
            default:
                break;
        }
    }

}
new Calculator(document).init();