JS面试之对象(2)

JS面试之对象(2)_第1张图片

前言

一篇彻底搞懂对象,从此不用担心没对象啦;
本文从对象定义方法,对象属性,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的作用:
1.创了一个新对象;
2.this指向构造函数;
3.构造函数有返回,会替换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 三种方法的优缺点

1.功能:都能实现对象的声明,并能够赋值和取值
2.继承性:内置方法创建的对象继承到__proto__属性上
3.隐藏属性:三种声明方法会默认为内部的每个成员(属性或方法)生成一些隐藏属性,这些隐藏属性是可以读取和可配置的,属性分类见下面
4.属性读取:Object.getOwnPropertyDescriptor()或getOwnPropertyDescriptor()
5.属性设置:Object.definePropertype或Object.defineProperties

2.对象的属性

2.1 属性分类

1.数据属性4个特性:
configurable(可配置),enumerable(可枚举),writable(可修改),value(属性值)

2.访问器属性2个特性:
get(获取),set(设置)

3.内部属性
由JavaScript引擎内部使用的属性;
不能直接访问,但是可以通过对象内置方法间接访问,如:[[Prototype]]可以通过 Object.getPrototypeOf()访问;
内部属性用[[]]包围表示,是一个抽象操作,没有对应字符串类型的属性名,如[[Prototype]].

2.2 属性描述符

1.定义:将一个属性的所有特性编码成一个对象返回
2.描述符的属性有:数据属性和访问器属性
3.使用范围:
作为方法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 属性定义

1.定义属性的函数有两个:Object.defineProperty和Object.defineProperties.例如:
Object.defineProperty(obj, propName, desc)

2.在引擎内部,会转换成这样的方法调用:
obj.[[DefineOwnProperty]](propName, desc, true)

2.5 属性赋值

1.赋值运算符(=)就是在调用[[Put]].比如:
obj.prop = v;

2.在引擎内部,会转换成这样的方法调用:
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 用法

1.不能与其他类型的值进行运算;
2.作为属性名

let mySymbol = Symbol();

// 第一种写法
var a = {};
a[mySymbol] = 'Hello!';

// 第二种写法
var a = {
  [mySymbol]: 'Hello!'
};

// 第三种写法
var a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });

// 以上写法都得到同样结果
a[mySymbol] // "Hello!"

3.作为对象属性名时,不能用点运算符,可以用[]

let a = {};
let name = Symbol();
a.name = 'lili';
a[name] = 'lucy';
console.log(a.name,a[name]); 

4.遍历不会被for...in、for...of和Object.keys()、Object.getOwnPropertyNames()取到该属性

3.4 Symbol.for

1.定义:在全局中搜索有没有以该参数作为名称的Symbol值,如果有,就返回这个Symbol值,否则就新建并返回一个以该字符串为名称的Symbol值
2.举例:

var s1 = Symbol.for('foo');
var s2 = Symbol.for('foo');
s1 === s2 // true

3.5 Symbol.keyFor

1.定义:返回一个已登记的Symbol类型值的key
2.举例:

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属性
2.只有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

1.定义:将源对象(source)的所有可枚举属性,复制到目标对象(target)
2.用法:

合并多个对象
var target = { a: 1, b: 1 };
var source1 = { b: 2, c: 2 };
var source2 = { c: 3 };
Object.assign(target, source1, source2);

3.注意:
这个是伪深度拷贝,只能拷贝第一层

5.2 JSON.stringify

1.原理:是将对象转化为字符串,而字符串是简单数据类型

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

1.ES5出来的方法;
2.三个参数:对象(必填),属性值(必填),描述符(可选);
3.defineProterty的描述符属性

数据属性:value,writable,configurable,enumerable
访问器属性:get,set
注:不能同时设置value和writable,这两对属性是互斥的

4.拦截对象的两种情况:

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);

5.拦截数组变化的情况

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无法检测数组索引赋值,改变数组长度的变化;
    但是通过数组方法来操作可以检测到


6.存在的问题

不能监听数组索引赋值和改变长度的变化
必须深层遍历嵌套的对象,因为defineProterty只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历,如果属性值也是对象那么需要深度遍历,显然能劫持一个完整的对象是更好的选择

6.2 proxy

1.ES6出来的方法,实质是对对象做了一个拦截,并提供了13个处理方法
13个方法详情请戳,阮一峰的proxy介绍

2.两个参数:对象和行为函数

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;

3.问题和优点
reflect对象没有构造函数
可以监听数组索引赋值,改变数组长度的变化,
是直接监听对象的变化,不用深层遍历

6.3 defineProterty和proxy的对比

1.defineProterty是es5的标准,proxy是es6的标准;

2.proxy可以监听到数组索引赋值,改变数组长度的变化;

3.proxy是监听对象,不用深层遍历,defineProterty是监听属性;

3.利用defineProterty实现双向数据绑定(vue2.x采用的核心)
请戳,剖析Vue原理&实现双向绑定MVVM
4.利用proxy实现双向数据绑定(vue3.x会采用)

你可能感兴趣的:(JS面试之对象(2))