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