JS基础-4|原生对象,ES5、ES6及后续版本对象拓展方法和深拷贝的实现方式

1,261 阅读10分钟

对象和数组一样,都是常用的东西。本篇对JS的对象做深入的分析和沉淀

原生JS中的对象

业务开发中,对象是最常见的数据类型。在JS中几乎“所有事物”都是对象,包括但不限于布尔、数字、字符串、日期、函数、对象等等。

字符串、数值和布尔值的定义

在实际开发中布尔、数字、字符串等类型都是直接赋值,并未用new关键字进行过定义。

var a = 1; // 1
var b = new Number(2); // Number对象
var c = new Object(3); // Number对象
console.log(a + b + c); // 6

直接赋值数字时不是Number对象,仅为数值。

在W3C的JavaScript 对象中文章末尾有一节:

请不要把字符串、数值和布尔值声明为对象!

如果通过关键词 "new" 来声明 JavaScript 变量,则该变量会被创建为对象:

var x = new String();        // 把 x 声明为 String 对象
var y = new Number();        // 把 y 声明为 Number 对象
var z = new Boolean();       //	把 z 声明为 Boolean 对象

请避免字符串、数值或逻辑对象。他们会增加代码的复杂性并降低执行速度。

这就是为什么不用new定义数字、字符串、布尔的原因:增加代码的复杂性并降低执行速度

定义对象变量

常见定义变量的方式:

var obj = { key: 'value' }

上述代码是创建对象最简单的方式,称为对象字面量方式创建对象。

原生JS还支持以下方式:

var obj = new Object();
obj.key = 'value'

官方描述:
使用new关键字进行变量定义。和使用字面量定义的方式,字面量有明显的优势:
{}方式具备简易性、可读性和更快执行速度

在ES5中支持以下方式:

var obj = { name: '张三' }
var obj2 = Object.create(obj);
obj2.key = 'value'

引用传递

对象通过引用传递,而非值传递。

基础数据类型是值传递,而非引用传递

// 字符串
var str = '1';
var str1 = str;
str1 = '2';
console.log(str); // 1
console.log(str1); // 2

// 数值
var num = 1;
var num1 = num;
num1 = 2;
console.log(num); // 1
console.log(num1); // 2

// 布尔值
var bool = true;
var bool1 = bool;
bool1 = false;
console.log(bool); // true
console.log(bool1); // false

// 对象(重新赋值)
var obj = { key: 'value' }
var obj1 = obj;
obj1 = { key1: 'value1' };
console.log(obj); // { key: 'value' }
console.log(obj1); // { key1: 'value1' }

// 对象(添加修改值)
var obj = { key: 'value' }
var obj1 = obj;
obj1.key = 'value_copy';
obj1.key1 = 'value1'
console.log(obj); // {key: 'value_copy', key1: 'value1'}
console.log(obj1); // {key: 'value_copy', key1: 'value1'}

ES5对象新增特性

ES5中新增了许多对象的方法,Vue也是基于ES5对象的方法实现的双向绑定。

GetterSetter JavaScript访问器

GetterSetter 想必都不陌生,Vue2双向绑定原理的知识点,用于数据劫持。

var obj = {
  name: '张三',
  language: "en",
  get lang() {
    return this.language.toUpperCase();
  },
  get hello() {
    console.log("I'm " + this.name)
  },
  set change(value) {
    this.name = value
  }
}
obj.hello // I'm 张三
console.log(obj.lang) // EN
obj.change = '李四';
obj.hello // I'm 李四

GetterSetter 的特点

  • 更简洁的语法(无需执行方法)
  • 允许属性和方法的语法相同()
  • 确保更好的数据质量(不会改变对象的原属性)
  • 有利于后台工作(返回后台处理后的数据)

以现有对象为原型创建对象: Object.create()

以现有对象为原型创建对象

var man = {
  sex: 1,
  say: function () {
    console.log(this.name)
  }
};
var person = Object.create(man);
person.name = '张三';
console.log(person); // { name: '张三' }
person.say(); // 张三

原生JS实现create:

function create(proto) {
	var obj = {};
	obj.__proto__ = proto;
	return obj;
}

var man = {
  sex: 1,
  say: function () {
    console.log(this.name)
  }
};
var person = create(man);
person.name = '张三';
console.log(person); // { name: '张三' }
person.say(); // 张三

添加或更改对象属性:definePropertydefineProperties

这个方法都不陌生,Vue2就是基于ES5的defineProperty实现数据劫持的。

defineProperty用于添加或更改对象属性,允许更改 getter 和 setter

var obj = { name: "张三" };
// 修改属性
Object.defineProperty(obj, "name", { value: "李四" }); // { name: '李四' }

// 添加属性:不可修改、不可枚举、不可配置
Object.defineProperty(obj, "age", {
  value: 18 
}); // {name: '李四', age: 18}
obj.age = 20; // {name: '李四', age: 18}

// 添加属性:可修改、不可枚举、不可配置
Object.defineProperty(obj, "age", { value: 18, writable: true });
obj.age = 20; // {name: '李四', age: 20}

// 添加属性:可修改、可枚举、不可配置
Object.defineProperty(obj, "age", { value: 18, writable: true, enumerable : true });
obj.age = 20; // {name: '李四', age: 20}

// 添加属性:全部支持
Object.defineProperty(obj, "age", { 
  value: 18,
  writable: true,
  enumerable: true,
  configurable: true 
});
obj.age = 20; // {name: '李四', age: 20}
Object.defineProperty(obj, "age", { value: 18 }); // {name: '李四', age: 18}

上述修改、枚举和配置:

  • writable:控制是否可以修改属性值。
  • enumerable:控制显式或隐式添加属性。
  • configurable:控制是否可以通过defineProperty重新定义属性。

通过defineProperty更改 getter 和 setter

// html
<div id="root"></div>

// js
var obj = {};
var vm = {};
const el = document.getElementById('root');
Object.defineProperty(vm, 'name', {
  get: function () {
    return obj.name;
  },
  set: function (value) {
    obj.name = value;
    el.innerHTML = value
  }
})
vm.name = '张三'
console.log(vm);
/*
{
  name: "张三"
  get name: ƒ ()
  set name: ƒ (value)
}
*/

上述例子简单模拟Vue数据绑定到dom的实现,仅模拟Vue数据劫持的核心实现。

defineProperties方法和defineProperty类似,区别在于defineProperties可添加或修改多个属性。

访问属性: Object.getOwnPropertyDescriptor()

返回指定对象上一个自有对应的属性

var obj = {
  name: "张三"
};
var result = Object.getOwnPropertyDescriptor(obj, 'name');
console.log(result); 
// {value: '张三', writable: true, enumerable: true, configurable: true}

返回元信息,可以在修改属性值前可用于检测,业务中不怎么用。

获取对象的属性名: Object.getOwnPropertyNames() 和bje Object.keys()

var obj = {
  name: '张三',
  age: 18
};
Object.defineProperty(obj, "sex", { value: 1 });
console.log(Object.getOwnPropertyNames(obj)); // ['name', 'age', 'sex']
console.log(Object.keys(obj)); // ['name', 'age']

通过 defineProperty 添加的属性没有设置可枚举属性,所以Object.keys() 不会返回。

区别Object.getOwnPropertyNames()可以获取所有属性名,而 Object.keys() 只能获取枚举的属性名

访问原型: Object.getPrototypeOf()

返回其原型的对象,可用于原型对比

var obj = { name: "张三" };
var obj2 = Object.create(obj);
console.log(Object.getPrototypeOf(obj2) === obj);

// 构造函数拓展
function People() { }
function Robot() { }
var bob = new People();
console.log(Object.getPrototypeOf(bob) === People.prototype); // true
console.log(Object.getPrototypeOf(bob) === Robot.prototype); // false

基于getPrototypeOf可以检测某个实例是否基于指定原型创建。

保护对象

用于禁止修改对象,保护后,上述命令将不可用。实际使用很少,感兴趣的写写demo吧。

// 防止向对象添加属性
Object.preventExtensions(object)

// 如果属性可以添加到对象,则返回 true
Object.isExtensible(object)

// 防止更改对象属性(不是值)
Object.seal(object)

// 如果对象被密封,则返回 true
Object.isSealed(object)

// 防止对对象进行任何更改
Object.freeze(object)

// 如果对象被冻结,则返回 true
Object.isFrozen(object)

ES6新增特性

ES6对象进行了比较大的升级,拓展了对象的能力。

属性升级:属性简洁表示法和属性名表达式

属性简洁表示法:对象可以直接写入变量和函数,作为对象的属性和方法。

var name = '张三';
var say = function () {
  console.log("I'm " + this.name);
}
var obj = { name, say };
console.log(obj); // { name: '张三‘, say: f() }
obj.say(); // I'm 张三

属性名表达式:之前对象的key只能是字符串,现在可以写表达式。

var key = 'name';
var obj = {
  [key]: '张三',
  ['a' + 'g' + 'e']: 18,
  ['say']: function () {
    console.log("I'm " + this.name);
  }
}

super关键字

super关键字:类似JS,指向当前对象的原型对象

var man = {
  say: function () {
    console.log("I'm " + this.name)
  }
};
var people = {
  name: '张三',
  say () {
    super.say()
  }
}
Object.setPrototypeOf(people, man);
people.say()

super关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。

扩展运算符、解构赋值(ES8)

这个相信无论你用什么框架都会经常使用的js技能之一。

需要注意的是对象的拓展运算符是在ES8中引入的,并非ES6

var obj = { a: 1, b: 2, c: 3 };
var obj2 = { ...obj, d: 4 };
console.log(obj2); // {a: 1, b: 2, c: 3, d: 4}
var { a, ...c } = obj;
console.log(c); // {b: 2, c: 3}

比较值是否相等:Object.is()

原生JS比较两个值是否相等使用的是=====

前者会自动转换数据类型,后者的NaN不等于自身。

Object.is()方法类似于===,用来比较两个值是否严格相等。

// NaN
console.log(NaN === NaN); // false
Object.is(NaN, NaN); // true

// +0和-0
console.log(+0 === -0); // true
Object.is(+0, -0); // false

// {};
console.log({} === {}); // false
Object.is({}, {}); // false

区别:+0,-0和NaN的判断结果不一致

原生js实现

function is(x, y) {
  if (x === y) {
    // 针对+0 不等于 -0的情况
    return x !== 0 || 1 / x === 1 / y;
  }
  // 针对NaN的情况
  return x !== x && y !== y;
}
console.log(is(NaN, NaN)) // true
console.log(is(0, 0)) // true
console.log(is(0, -0)) // false
console.log(is({}, {})) // false

合并对象:Object.assign()

这个方法都不陌生,用于对象的合并。

// 基础数据类型
var obj1 = {}
var obj2 = {
  key1: 1,
  key2: '2',
  key3: true
}
Object.assign(obj1, obj2);
console.log(obj1); // {key1: 1, key2: '2', key3: true}
obj2.key3 = false;
console.log(obj1); // {key1: 1, key2: '2', key3: true}

// 复杂数据类型
var obj3 = {}
var obj4 = {
  key1: {},
  key2: [1, 2, 3],
  key3: {
    key3_1: [1, 2, 3]
  }
}
Object.assign(obj3, obj4);
console.log(JSON.stringify(obj3, null, 4));
// {
//   "key1": {},
//   "key2": [ 1, 2, 3 ],
//   "key3": {
//     "key3_1": [ 1, 2, 3 ]
//   }
// }
obj4.key2 = []
obj4.key3.key3_1 = {};
console.log(JSON.stringify(obj3, null, 4));
// {
//   "key1": {},
//   "key2": [ 1, 2, 3 ],
//   "key3": {
//     "key3_1": {}
//   }
// }

总结:

  • 如果对象的属性值为基础类型(如string, number),Object.assign()为深拷贝
  • 如果属性值为对象或其它引用类型,Object.assign()为浅拷贝

设置对象的原型对象: setPrototypeOf

Object.setPrototypeOf方法的作用与__proto__相同,用来设置一个对象的原型对象(prototype),返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法。

var man = {
  sex: 1
};
var person = {
  name: '张三'
};
Object.setPrototypeOf(person, man);
console.log(person); // { name: '张三' }

注意:ES6以后建议不要再使用__proto__,使用Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替。

ES7及后续版本新特性

ES6后的版本更新基本都是完善对象的方法集。

返回对象所有自身属性的描述对象(ES2017):Object.getOwnPropertyDescriptors()

Object.getOwnPropertyDescriptor类似,这个是返回对象属性的描述对象(全部)。

var obj = {
  id: 1,
  name: '张三'
};
Object.defineProperty(obj, 'sex', { value: 1 })
console.log(Object.getOwnPropertyDescriptors(obj));
// {
//   id: {
//     configurable: true,
//     enumerable: true,
//     value: 1,
//     writable: true
//   },
//   name: {
//     configurable: true,
//     enumerable: true,
//     value: "张三",
//     writable: true,
//   },
//   sex:{
//     configurable: false,
//     enumerable: false,
//     value: 1,
//     writable: false
//   }
// }

它会返回每个属性的描述对象

返回对象可遍历对象的键值数组(ES2017):Object.values()

Object.keys()类似的方法,返回对象键值数组

var obj = {
  id: 1,
  name: '张三'
};
Object.defineProperty(obj, 'sex', { value: 1 })
console.log(Object.values(obj)); // [1, '张三']

返回对象可遍历对象的键值对数组(ES2017): Object.entries()

类似Object.keys()Object.values()的集合,返回键值对数组

var obj = {
  id: 1,
  name: '张三'
};
Object.defineProperty(obj, 'sex', { value: 1 })
console.log(Object.entries(obj)); // [["id",1],["name","张三"]]

将键值对数组转为对象(ES2019): Object.fromEntries()

Object.entries()相反的操作,将键值对数组转为对象

console.log(JSON.stringify(Object.fromEntries([["id",1],["name","张三"]])));
// {"id":1,"name":"张三"}

拓展:深拷贝

深拷贝无论是在实际开发中还是面试中都是比较常见的问题,现阶段开发中,大多数情况下都会使用第三方库完成,如lodash库的deepClone方法。下面是几种深拷贝的方式:

var obj = {
  id: 1,
  name: '张三',
  tags: ['篮球', '电玩'],
  schools: [
    { id: 1, name: '清华大学' },
    { id: 2, name: '清华附中' }
  ],
  say: function () {
    console.log(this.name)
  }
}

// 未深拷贝时
var obj1 = obj;
obj.tags.push('学霸');
obj.schools.push({ id: 3, name: '清华附小' });
console.log(obj1)

// 1. 利用JSON实现
var obj2 = JSON.parse(JSON.stringify(obj));
obj.tags.push('学霸');
obj.schools.push({ id: 3, name: '清华附小' });
console.log(obj2)

// 2. 利用Object.create实现
function clone(data) {
  var obj = Object.create(Object.getPrototypeOf(data));
  var props = Object.getOwnPropertyNames(data);
  props.forEach(function(name) {
    Object.defineProperty(obj, name, Object.getOwnPropertyDescriptor(obj, name));
  });
  return obj;
}
var obj3 = deepCopy(obj);
obj.tags.push('学霸');
obj.schools.push({ id: 3, name: '清华附小' });
console.log(obj1)

// 2. 原生JS实现
function deepCopy(data) {
  var result = Array.isArray(data) ? [] : {};
  for (var k in data) {
    if (data.hasOwnProperty(k)) {
      if ( typeof data[ k ] === 'object' ) {
        result[k] = deepCopy(data[k]);
      } else {
        result[k] = data[ k ]
      }
    }
  }
  return result;
}
var obj4 = deepCopy(obj);
obj.tags.push('学霸');
obj.schools.push({ id: 3, name: '清华附小' });
console.log(obj1)
  • JSON的方式无法拷贝函数,若没有函数它算最简单的方式,
  • Object.create是基于ES5的特性实现,比原生的方式简单一些。
  • 最后是原生JS的实现方式,比前两种方式复杂一些。

相关资料

相关试题

  1. new Object(){}定义对象有什么区别?
  2. Vue双向绑定的原理是什么?
  3. Vue数据劫持的具体实现逻辑是什么?
  4. 通过某个构造函数创建的变量,如何检测它是这个构造函数的实例?
  5. ES6中对象新增了哪些新特性?
  6. 你知道super关键字吗?它和js有什么区别?
  7. 你知道怎么对比两个对象是否相等吗?
  8. ===== 有什么区别?
  9. Object.is()===有什么区别?
  10. Object.create()和构造函数的区别是什么?
  11. 你知道深拷贝和浅拷贝的区别吗?
  12. js中浅拷贝的方法有哪些?
  13. 如何实现对象的深拷贝?
  14. 实现一个深拷贝函数(笔试)。
  15. 如何获取对象的全部键名?