序列文章
JS面试之函数(1)
JS面试之数组的几个不low操作(3)
前言
一篇彻底搞懂对象,从此不用担心没对象啦;
本文从对象定义方法,对象属性,Symbol数据类型,遍历几种方法,对象拷贝,vue2.x和vue3.x拦截对象属性方法及代码实现几个方面由浅入深介绍对象
1.对象的声明方法
1.1 字面量
var test2 = {x:123,y:345};
console.log(test2);//{x:123,y:345};
console.log(test2.x);//123
console.log(test2.__proto__.x);//undefined
console.log(test2.__proto__.x === test2.x);//false
1.2 构造函数
var test1 = new Object({x:123,y:345});
console.log(test1);//{x:123,y:345}
console.log(test1.x);//123
console.log(test1.__proto__.x);//undefined
console.log(test1.__proto__.x === test1.x);//false
new的作用:
- 创了一个新对象;
- this指向构造函数;
- 构造函数有返回,会替换new出来的对象,如果没有就是new出来的对象
1.3 内置方法
Obejct.create(obj,descriptor),obj是对象,describe描述符属性(可选)
let test = Object.create({x:123,y:345});
console.log(test);//{}
console.log(test.x);//123
console.log(test.__proto__.x);//3
console.log(test.__proto__.x === test.x);//true
1.4 三种方法的优缺点
- 功能:都能实现对象的声明,并能够赋值和取值
- 继承性:内置方法创建的对象继承到proto属性上
- 隐藏属性:三种声明方法会默认为内部的每个成员(属性或方法)生成一些隐藏属性,这些隐藏属性是可以读取和可配置的,属性分类见下面
- 属性读取:Object.getOwnPropertyDescriptor()或getOwnPropertyDescriptor()
- 属性设置:Object.definePropertype或Object.defineProperties
2.对象的属性
2.1 属性分类
数据属性4个特性:
configurable(可配置),enumerable(可枚举),writable(可修改),value(属性值)访问器属性2个特性:
get(获取),set(设置)内部属性
由JavaScript引擎内部使用的属性;
不能直接访问,但是可以通过对象内置方法间接访问,如:[[Prototype]]可以通过 Object.getPrototypeOf()访问;
内部属性用[[]]包围表示,是一个抽象操作,没有对应字符串类型的属性名,如[[Prototype]].
2.2 属性描述符
- 定义:将一个属性的所有特性编码成一个对象返回
- 描述符的属性有:数据属性和访问器属性
- 使用范围:
作为方法Object.defineProperty, Object.getOwnPropertyDescriptor, Object.create的第二个参数,
2.3 属性描述符的默认值
1.访问对象存在的属性
特性名 | 默认值 |
---|---|
value | 对应属性值 |
get | 对应属性值 |
set | undefined |
writable | true |
enumerable | true |
configurable | true |
所以通过上面三种声明方法已存在的属性都是有这些默认描述符
2.访问对象不存在的属性
特性名 | 默认值 |
---|---|
value | undefined |
get | undefined |
set | undefined |
writable | false |
enumerable | false |
configurable | false |
2.3 描述符属性的使用规则
get,set与wriable,value是互斥的,如果有交集设置会报错
2.4 属性定义
定义属性的函数有两个:Object.defineProperty和Object.defineProperties.例如:
Object.defineProperty(obj, propName, desc)在引擎内部,会转换成这样的方法调用:
obj.[[DefineOwnProperty]](propName, desc, true)
2.5 属性赋值
赋值运算符(=)就是在调用[[Put]].比如:
obj.prop = v;在引擎内部,会转换成这样的方法调用:
obj.[[Put]]("prop", v, isStrictModeOn)
2.6 判断对象的属性
名称 | 含义 | 用法 |
---|---|---|
in | 如果指定的属性在指定的对象或其原型链中,则in 运算符返回true | 'name' in test //true |
hasOwnProperty() | 只判断自身属性 | test.hasOwnProperty('name') //true |
.或[] | 对象或原型链上不存在该属性,则会返回undefined | test.name //"lei" test["name"] //"lei" |
3.Symbol
3.1概念
是一种数据类型;
不能new,因为Symbol是一个原始类型的值,不是对象。
3.2 定义方法
Symbol(),可以传参
var s1 = Symbol();
var s2 = Symbol();
s1 === s2 // false
// 有参数的情况
var s1 = Symbol("foo");
var s2 = Symbol("foo");
s1 === s2 // false
3.3 用法
- 不能与其他类型的值进行运算;
- 作为属性名
let mySymbol = Symbol();
// 第一种写法
var a = {};
a[mySymbol] = 'Hello!';
// 第二种写法
var a = {
[mySymbol]: 'Hello!'
};
// 第三种写法
var a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
// 以上写法都得到同样结果
a[mySymbol] // "Hello!"
- 作为对象属性名时,不能用点运算符,可以用[]
let a = {};
let name = Symbol();
a.name = 'lili';
a[name] = 'lucy';
console.log(a.name,a[name]);
- 遍历不会被for...in、for...of和Object.keys()、Object.getOwnPropertyNames()取到该属性
3.4 Symbol.for
- 定义:在全局中搜索有没有以该参数作为名称的Symbol值,如果有,就返回这个Symbol值,否则就新建并返回一个以该字符串为名称的Symbol值
- 举例:
var s1 = Symbol.for('foo');
var s2 = Symbol.for('foo');
s1 === s2 // true
3.5 Symbol.keyFor
- 定义:返回一个已登记的Symbol类型值的key
- 举例:
var s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
var s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined
4.遍历
4.1 一级对象遍历方法
方法 | 特性 |
---|---|
for ... in | 遍历对象自身的和继承的可枚举属性(不含Symbol属性) |
Object.keys(obj) | 返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性) |
Object.getOwnPropertyNames(obj) | 返回一个数组,包括对象自身的所有可枚举属性(不含Symbol属性) |
Object.getOwnPropertySymbols(obj) | 返回一个数组,包含对象自身的所有Symbol属性 |
Reflect.ownKeys(obj) | 返回一个数组,包含对象自身的所有(不枚举、可枚举和Symbol)属性 |
Reflect.enumerate(obj) | 返回一个Iterator对象,遍历对象自身的和继承的所有可枚举属性(不含Symbol属性) |
总结:1. 只有Object.getOwnPropertySymbols(obj)和Reflect.ownKeys(obj)可以拿到Symbol属性
- 只有Reflect.ownKeys(obj)可以拿到不可枚举属性
4.2 多级对象遍历
数据模型:
var treeNodes = [
{
id: 1,
name: '1',
children: [
{
id: 11,
name: '11',
children: [
{
id: 111,
name: '111',
children:[]
},
{
id: 112,
name: '112'
}
]
},
{
id: 12,
name: '12',
children: []
}
],
users: []
},
];
递归:
var parseTreeJson = function(treeNodes){
if (!treeNodes || !treeNodes.length) return;
for (var i = 0, len = treeNodes.length; i < len; i++) {
var childs = treeNodes[i].children;
console.log(treeNodes[i].id);
if(childs && childs.length > 0){
parseTreeJson(childs);
}
}
};
console.log('------------- 递归实现 ------------------');
parseTreeJson(treeNodes);
5.深度拷贝
5.1 Object.assign
- 定义:将源对象(source)的所有可枚举属性,复制到目标对象(target)
- 用法:
合并多个对象
var target = { a: 1, b: 1 };
var source1 = { b: 2, c: 2 };
var source2 = { c: 3 };
Object.assign(target, source1, source2);
- 注意:
这个是伪深度拷贝,只能拷贝第一层
5.2 JSON.stringify
- 原理:是将对象转化为字符串,而字符串是简单数据类型
5.3 递归拷贝
function deepClone(source){
const targetObj = source.constructor === Array ? [] : {}; // 判断复制的目标是数组还是对象
for(let keys in source){ // 遍历目标
if(source.hasOwnProperty(keys)){
if(source[keys] && typeof source[keys] === 'object'){ // 如果值是对象,就递归一下
targetObj[keys] = source[keys].constructor === Array ? [] : {};
targetObj[keys] = deepClone(source[keys]);
}else{ // 如果不是,就直接赋值
targetObj[keys] = source[keys];
}
}
}
return targetObj;
}
6.数据拦截
定义:利用对象内置方法,设置属性,进而改变对象的属性值
6.1 Object.defineProterty
- ES5出来的方法;
- 三个参数:对象(必填),属性值(必填),描述符(可选);
- defineProterty的描述符属性
数据属性:value,writable,configurable,enumerable
访问器属性:get,set
注:不能同时设置value和writable,这两对属性是互斥的
- 拦截对象的两种情况:
let obj = {name:'',age:'',sex:'' },
defaultName = ["这是姓名默认值1","这是年龄默认值1","这是性别默认值1"];
Object.keys(obj).forEach(key => {
Object.defineProperty(obj, key, {
get() {
return defaultName;
},
set(value) {
defaultName = value;
}
});
});
console.log(obj.name);
console.log(obj.age);
console.log(obj.sex);
obj.name = "这是改变值1";
console.log(obj.name);
console.log(obj.age);
console.log(obj.sex);
let objOne={},defaultNameOne="这是默认值2";
Object.defineProperty(obj, 'name', {
get() {
return defaultNameOne;
},
set(value) {
defaultNameOne = value;
}
});
console.log(objOne.name);
objOne.name = "这是改变值2";
console.log(objOne.name);
- 拦截数组变化的情况
let a={};
bValue=1;
Object.defineProperty(a,"b",{
set:function(value){
bValue=value;
console.log("setted");
},
get:function(){
return bValue;
}
});
a.b;//1
a.b=[];//setted
a.b=[1,2,3];//setted
a.b[1]=10;//无输出
a.b.push(4);//无输出
a.b.length=5;//无输出
a.b;//[1,10,3,4,undefined];
结论:defineProperty无法检测数组索引赋值,改变数组长度的变化;
但是通过数组方法来操作可以检测到
- 存在的问题
不能监听数组索引赋值和改变长度的变化
必须深层遍历嵌套的对象,因为defineProterty只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历,如果属性值也是对象那么需要深度遍历,显然能劫持一个完整的对象是更好的选择
6.2 proxy
ES6出来的方法,实质是对对象做了一个拦截,并提供了13个处理方法
13个方法详情请戳,阮一峰的proxy介绍两个参数:对象和行为函数
let handler = {
get(target, key, receiver) {
console.log("get", key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log("set", key, value);
return Reflect.set(target, key, value, receiver);
}
};
let proxy = new Proxy(obj, handler);
proxy.name = "李四";
proxy.age = 24;
- 问题和优点
reflect对象没有构造函数
可以监听数组索引赋值,改变数组长度的变化,
是直接监听对象的变化,不用深层遍历
6.3 defineProterty和proxy的对比
defineProterty是es5的标准,proxy是es6的标准;
proxy可以监听到数组索引赋值,改变数组长度的变化;
proxy是监听对象,不用深层遍历,defineProterty是监听属性;
利用defineProterty实现双向数据绑定(vue2.x采用的核心)
利用proxy实现双向数据绑定(vue3.x会采用)