JS三大对象
JavaScript有3大对象,分别是本地对象
、内置对象
和宿主对象
。
在此引用ECMA-262(ECMAScript的制定标准)对于他们的定义:
- 本地对象
- 与宿主无关,独立于宿主环境的ECMAScript实现提供的对象。
- 简单来说,本地对象就是 ECMA-262 定义的类(引用类型)。
- 这些引用类型在运行过程中需要通过new来创建所需的实例对象。
- 包含:
Object
、Array
、Date
、RegExp
、Function
、Boolean
、Number
、String
等。
- 内置对象
- 与宿主无关,独立于宿主环境的ECMAScript实现提供的对象。
- 在 ECMAScript 程序开始执行前就存在,本身就是实例化内置对象,开发者无需再去实例化。
- 内置对象是本地对象的子集。
- 包含:
Global
和Math
。 - ECMAScript5中增添了
JSON
这个存在于全局的内置对象。
- 宿主对象
- 由 ECMAScript 实现的宿主环境提供的对象,包含两大类,一个是宿主提供,一个是自定义类对象。
- 所有非本地对象都属于宿主对象。
- 对于嵌入到网页中的JS来说,其宿主对象就是浏览器提供的对象,浏览器对象有很多,如
Window
和Document
等。 - 所有的
DOM
和BOM
对象都属于宿主对象。关于专业名词:本地对象也经常被叫做原生对象或内部对象,包含Global和Math在内的内置对象在《JavaScript高级程序设计》里也被叫做单体内置对象,很多时候,干脆也会直接把本地对象和内置对象统称为“内置对象”,也就是说除了宿主对象,剩下的都是ECMAScript的内部的“内置”对象。 声明:本文也将采取这种统称为“内置对象”的方式,比如文章标题。
定义一个对象
创建一个对象
对象字面量直接声明。
const obj = {
firstProperty: 'firstProperty',
123: 123,
1.2: 1.2,
'1.2.3': '1.2.3',
};
/*{
1.2: 1.2
1.2.3: "1.2.3"
123: 123
firstProperty: "firstProperty"
}*/
使用函数模拟以减少创建对象时的重复代码
- 工厂模式,工厂模式的主要工作原理是用函数来封装创建对象的细节,从而通过调用函数来达到复用的目的。但是它有一个很大的问题就是创建出来的对象无法和某个类型联系起来,它只是简单的封装了复用代码,而没有建立起对象和类型间的关系。
- 构造函数模式。js 中每一个函数都可以作为构造函数,只要一个函数是通过 new 来调用的,那么我们就可以把它称为构造函数。执行构造函数首先会创建一个对象,然后将对象的原型指向构造函数的 prototype 属性,然后将执行上下文中的 this 指向这个对象,最后再执行整个函数,如果返回值不是对象,则返回新建的对象。因为 this 的值指向了新建的对象,因此我们可以使用 this 给对象赋值。构造函数模式相对于工厂模式的优点是,所创建的对象和构造函数建立起了联系,因此我们可以通过原型来识别对象的类型。但是构造函数存在一个缺点就是,造成了不必要的函数对象的创建,因为在 js 中函数也是一个对象,因此如果对象属性中如果包含函数的话,那么每次我们都会新建一个函数对象,浪费了不必要的内存空间,因为函数是所有的实例都可以通用的。
- 原型模式,因为每一个函数都有一个 prototype 属性,这个属性是一个对象,它包含了通过构造函数创建的所有实例都能共享的属性和方法。因此我们可以使用原型对象来添加公用属性和方法,从而实现代码的复用。这种方式相对于构造函数模式来说,解决了函数对象的复用问题。但是这种模式也存在一些问题,一个是没有办法通过传入参数来初始化值,另一个是如果存在一个引用类型如 Array 这样的值,那么所有的实例将共享一个对象,一个实例对引用类型值的改变会影响所有的实例。
- 组合使用构造函数模式和原型模式,这是创建自定义类型的最常见方式。因为构造函数模式和原型模式分开使用都存在一些问题,因此我们可以组合使用这两种模式,通过构造函数来初始化对象的属性,通过原型对象来实现函数方法的复用。这种方法很好的解决了两种模式单独使用时的缺点,但是有一点不足的就是,因为使用了两种不同的模式,所以对于代码的封装性不够好。
- 动态原型模式,这一种模式将原型方法赋值的创建过程移动到了构造函数的内部,通过对属性是否存在的判断,可以实现仅在第一次调用函数时对原型对象赋值一次的效果。这一种方式很好地对上面的混合模式进行了封装。
- 寄生构造函数模式,这一种模式和工厂模式的实现基本相同,我对这个模式的理解是,它主要是基于一个已有的类型,在实例化时对实例化的对象进行扩展。这样既不用修改原来的构造函数,也达到了扩展对象的目的。它的一个缺点和工厂模式一样,无法实现对象的识别。
- es6 Class
详细资料可以参考: 《JavaScript 深入理解之对象创建》
修改对象的参数
const obj = {};
obj.firstProperty = 'firstProperty';
obj[123] = 123;
obj[1.2] = 1.2;
obj['1.2.3'] = '1.2.3';
delete obj[123]
面向对象
封装
继承
- 以原型链的方式来实现继承,但是这种实现方式存在的缺点是,在包含有引用类型的数据时,会被所有的实例对象所共享,容易造成修改的混乱。还有就是在创建子类型的时候不能向超类型传递参数。
- 使用借用构造函数的方式,这种方式是通过在子类型的函数中调用超类型的构造函数来实现的,这一种方法解决了不能向超类型传递参数的缺点,但是它存在的一个问题就是无法实现函数方法的复用,并且超类型原型定义的方法子类型也没有办法访问到。
- 组合继承,组合继承是将原型链和借用构造函数组合起来使用的一种方式。通过借用构造函数的方式来实现类型的属性的继承,通过将子类型的原型设置为超类型的实例来实现方法的继承。这种方式解决了上面的两种模式单独使用时的问题,但是由于我们是以超类型的实例来作为子类型的原型,所以调用了两次超类的构造函数,造成了子类型的原型中多了很多不必要的属性。
- 原型式继承,原型式继承的主要思路就是基于已有的对象来创建新的对象,实现的原理是,向函数中传入一个对象,然后返回一个以这个对象为原型的对象。这种继承的思路主要不是为了实现创造一种新的类型,只是对某个对象实现一种简单继承,ES5 中定义的 Object.create() 方法就是原型式继承的实现。缺点与原型链方式相同。
- 寄生式继承,寄生式继承的思路是创建一个用于封装继承过程的函数,通过传入一个对象,然后复制一个对象的副本,然后对象进行扩展,最后返回这个对象。这个扩展的过程就可以理解是一种继承。这种继承的优点就是对一个简单对象实现继承,如果这个对象不是我们的自定义类型时。缺点是没有办法实现函数的复用。
- 寄生式组合继承,组合继承的缺点就是使用超类型的实例做为子类型的原型,导致添加了不必要的原型属性。寄生式组合继承的方式是使用超类型的原型的副本来作为子类型的原型,这样就避免了创建不必要的属性。
- es6 extends
寄生式组合继承的实现
```javascript function Person(name) { this.name = name; }
Person.prototype.sayName = function() { console.log(“My name is “ + this.name + “.”); };
function Student(name, grade) { Person.call(this, name); this.grade = grade; }
Student.prototype = Object.create(Person.prototype); Student.prototype.constructor = Student;
Student.prototype.sayMyGrade = function() { console.log(“My grade is “ + this.grade + “.”); };
<a name="x20oz"></a>
## 多态
<a name="Q9OgP"></a>
# Object的方法
<a name="h4Ei6"></a>
## [Object.defineProperty](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty)
目的是为了可以更加精确的为对象天价属性<br />数据绑定<br />数据劫持
<a name="YdNzs"></a>
## [Object.assgn](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)
```javascript
Object.assign(target, ...sources)
如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。后面的源对象的属性将类似地覆盖前面的源对象的属性。
Object.assign
方法只会拷贝源对象自身的并且可枚举的属性到目标对象。该方法使用源对象的[[Get]]
和目标对象的[[Set]]
,所以它会调用相关 getter 和 setter。因此,它分配属性,而不仅仅是复制或定义新的属性。如果合并源包含getter,这可能使其不适合将新属性合并到原型中。为了将属性定义(包括其可枚举性)复制到原型,应使用Object.getOwnPropertyDescriptor()
和Object.defineProperty()
。String
类型和 Symbol
类型的属性都会被拷贝。
在出现错误的情况下,例如,如果属性不可写,会引发TypeError
,如果在引发错误之前添加了任何属性,则可以更改target
对象。
注意,Object.assign
不会在那些source
对象值为 null
或 undefined
的时候抛出错误。
示例1
方法返回值是原对象的引用
const test1 = {a: 1, b: 2}
const test2 = {b: 3, c: 4}
const test3 = {c: 4, d: 5}
const test4 = Object.assign(test1, test2, test3)
console.log(test1)
console.log(test4)
console.log(test1 === test4)
test3.__proto__.e=99
test1.d = 400
console.log(test4)
test3.__proto__.e = 99 =(等价)=> Object.prototype.e = 99
test3.__proto__ => Object.prototype
// 例子中在实例化了对象之后,更改了其原型,实际开发中不会这么做
如果 test3 通过下面来定义
function Test(){
this.c = 5;
this.d = 6
}
const test3 = new Test()
// 此时 test4 呢?【不会加到test4的原型上】
const obj = Object.assign({}, {
[Symbol("a")]: 1
})
console.log(obj)
console.log(Object.getOwnPropertySymbols(obj))
继承属性和不可枚举属性是不可以被拷贝的。
const obj = Object.create({foo: 1}, { // foo 是个继承属性。
bar: {
value: 2 // bar 是个不可枚举属性。
},
baz: {
value: 3,
enumerable: true // baz 是个自身可枚举属性。
}
});
const copy = Object.assign({}, obj);
console.log(copy); // { baz: 3 }
原始类型会被包装为对象
const v1 = "abc";
const v2 = true;
const v3 = 10;
const v4 = Symbol("foo")
const v5 = function test() {}
const r1 = new String(v1)
const r2 = new Boolean(v2)
const r3 = new Number(v3)
// symbol 不能 new
const r4 = new Symbol(v4)
const r5 = new Function(v5)
const obj = Object.assign({}, v1, null, v2, undefined, v3, v4, v5);
// 原始类型会被包装,null 和 undefined 会被忽略。
// 注意,只有字符串的包装对象才可能有自身可枚举属性。
console.log(obj); // { "0": "a", "1": "b", "2": "c" }
异常会打断后续的拷贝任务
const target = Object.defineProperty({}, "foo", {
value: 1,
writable: false
}); // target 的 foo 属性是个只读属性。
Object.assign(target, {bar: 2}, {foo2: 3, foo: 3, foo3: 3}, {baz: 4});
// TypeError: "foo" is read-only
// 注意这个异常是在拷贝第二个源对象的第二个属性时发生的。
console.log(target.bar); // 2,说明第一个源对象拷贝成功了。
console.log(target.foo2); // 3,说明第二个源对象的第一个属性也拷贝成功了。
console.log(target.foo); // 1,只读属性不能被覆盖,所以第二个源对象的第二个属性拷贝失败了。
console.log(target.foo3); // undefined,异常之后 assign 方法就退出了,第三个属性是不会被拷贝到的。
console.log(target.baz); // undefined,第三个源对象更是不会被拷贝到的。
拷贝访问器
const obj = {
foo: 1,
get bar() {
return 2;
}
};
let copy = Object.assign({}, obj);
console.log(copy); // { foo: 1, bar: 2 } copy.bar的值来自obj.bar的getter函数的返回值
// 下面这个函数会拷贝所有自有属性的属性描述符
function completeAssign(target, ...sources) {
sources.forEach(source => {
let descriptors = Object.keys(source).reduce((descriptors, key) => {
descriptors[key] = Object.getOwnPropertyDescriptor(source, key);
return descriptors;
}, {});
// Object.assign 默认也会拷贝可枚举的Symbols
Object.getOwnPropertySymbols(source).forEach(sym => {
let descriptor = Object.getOwnPropertyDescriptor(source, sym);
if (descriptor.enumerable) {
descriptors[sym] = descriptor;
}
});
Object.defineProperties(target, descriptors);
});
return target;
}
copy = completeAssign({}, obj);
console.log(copy);
// { foo:1, get bar() { return 2 } }
注意的问题
- 针对深拷贝,需要使用其他办法,因为
Object.assign()
拷贝的是(可枚举)属性值。假如源值是一个对象的引用,它仅仅会复制其引用值。 - 目标对象自身也会改变。
- 合并具有相同属性名的属性,属性被后续参数中具有相同属性的其他对象覆盖。
- 可以拷贝symbol类型的属性
- 继承属性和不可枚举属性是不能被拷贝的。
- 原始类型会被包装为对象。
- 异常会打断后续拷贝任务。
- 拷贝访问器。
Object.freeze
Object.freeze()
方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze()
返回和传入的参数相同的对象。
- 不能被修改
- 不能添加删除新的属性
- 不能修改属性值
- 不能修改原型
不可以修改对象属性的可枚举性,可配置性,可写性
是一个浅操作,复杂的对象,可能需要递归处理
- 仅仅让传入它的对象
immutable
(可以对变量重新赋值)
示例
var obj = {
prop: function() {},
foo: 'bar'
};
// 新的属性会被添加, 已存在的属性可能
// 会被修改或移除
obj.foo = 'baz';
obj.lumpy = 'woof';
delete obj.prop;
// 作为参数传递的对象与返回的对象都被冻结
// 所以不必保存返回的对象(因为两个对象全等)
var o = Object.freeze(obj);
o === obj; // true
Object.isFrozen(obj); // === true
// 现在任何改变都会失效
obj.foo = 'quux'; // 静默地不做任何事
// 静默地不添加此属性
obj.quaxxor = 'the friendly duck';
// 在严格模式,如此行为将抛出 TypeErrors
function fail(){
'use strict';
obj.foo = 'sparky'; // throws a TypeError
delete obj.quaxxor; // 返回true,因为quaxxor属性从来未被添加
obj.sparky = 'arf'; // throws a TypeError
}
fail();
// 试图通过 Object.defineProperty 更改属性
// 下面两个语句都会抛出 TypeError.
Object.defineProperty(obj, 'ohai', { value: 17 });
Object.defineProperty(obj, 'foo', { value: 'eit' });
// 也不能更改原型
// 下面两个语句都会抛出 TypeError.
Object.setPrototypeOf(obj, { x: 20 })
obj.__proto__ = { x: 20 }
let a = [0];
Object.freeze(a); // 现在数组不能被修改了.
a[0]=1; // fails silently
a.push(2); // fails silently
// In strict mode such attempts will throw TypeErrors
function fail() {
"use strict"
a[0] = 1;
a.push(2);
}
fail();
obj1 = {
internal: {}
};
Object.freeze(obj1);
obj1.internal.a = 'aValue';
obj1.internal.a // 'aValue'
场景
- Vue开发中的代码优化?
- immutable-js
- react的函数式编程
深冻结函数
// 深冻结函数.
function deepFreeze(obj) {
// 取回定义在obj上的属性名
var propNames = Object.getOwnPropertyNames(obj);
// 在冻结自身之前冻结属性
propNames.forEach(function(name) {
var prop = obj[name];
// 如果prop是个对象,冻结它
if (typeof prop == 'object' && prop !== null)
deepFreeze(prop);
});
// 冻结自身(no-op if already frozen)
return Object.freeze(obj);
}
obj2 = {
internal: {}
};
deepFreeze(obj2);
obj2.internal.a = 'anotherValue';
obj2.internal.a; // undefined
Object.isFrozen
**Object.isFrozen()**
方法判断一个对象是否被冻结。
在 ES5 中,如果参数不是一个对象类型,将抛出一个TypeError
异常。在 ES2015 中,非对象参数将被视为一个冻结的普通对象,因此会返回true
。
Object.seal
**Object.seal()**
方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要原来是可写的就可以改变。
- 是一个浅操作,复杂的对象,可能需要递归处理
- 可以重新赋值
示例
```javascript // 密封一个对象 var obj = { a: 1, b: function () { console.log(2); },
[Symbol('c')]: 3
};
Object.getOwnPropertyDescriptors(obj); / { a: {value: 1, writable: true, enumerable: true, configurable: true}, b: {value: ƒ, writable: true, enumerable: true, configurable: true}, Symbol(c): {value: 3, writable: true, enumerable: true, configurable: true} } /
Object.seal(obj); Object.getOwnPropertyDescriptors(obj); / { a: {value: 1, writable: true, enumerable: true, configurable: false}, b: {value: ƒ, writable: true, enumerable: true, configurable: false}, Symbol(c): {value: 3, writable: true, enumerable: true, configurable: false} } /
```javascript
// 修改一个密封对象
var obj = {
a: 1,
b: function () { console.log(2); },
[Symbol.for('c')]: 3
};
obj.a; // 1
obj.b(); // 2
obj[Symbol.for('c')]; // 3
Object.seal(obj);
// 尝试删除一个属性
delete obj.a; // false, 静默失败,严格模式下TypeError
obj.a; // 1
// 尝试修改一个属性
obj.a = 10;
obj.a; // 10 修改成功,密封操作只修改属性的可配置性,不影响可写性
// 尝试修改一个方法
obj.b = function () { return 20; }
obj.b(); // 20 修改成功,密封操作只修改方法的可配置性,不影响可写性
// 尝试修改数据属性成访问器属性
Object.defineProperty(obj, 'a', {
get: function () { return 1; }
};
// TypeError: Cannot redefine property: a
场景
React里的createRef借助这个实现
function createRef() {
var refObject = {
current: null
};
{
Object.seal(refObject);
}
return refObject;
}
Object.seal vs Object.freeze
相同点
- ES5新增。
- 对象不可能扩展,也就是不能再添加新的属性或者方法。
- 对象已有属性不允许被删除。
- 对象属性特性不可以重新配置。
不同点
- Object.seal方法生成的密封对象,如果属性是可写的,那么可以修改属性值。
- Object.freeze方法生成的冻结对象,属性都是不可写的,也就是属性值无法更改。
Object.isSealed
Object.isSealed()
方法判断一个对象是否被密封。
在ES5中,如果这个方法的参数不是一个对象(一个原始类型),那么它会导致TypeError
。在ES2015中,非对象参数将被视为是一个密封的普通对象,只返回true
。
问题
如何判断一个对象是否属于某个类
- 使用 instanceof 运算符来判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。
- 通过对象的 constructor 属性来判断,对象的 constructor 属性指向该对象的构造函数,但是这种方式不是很安全,因为 constructor 属性可以被改写。
- 第三种方式,如果需要判断的是某个内置的引用类型的话,可以使用 Object.prototype.toString() 方法来打印对象的[[Class]] 属性来进行判断。
instanceof 的作用
instanceof 运算符用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。 ```javascript // 实现:
function myInstanceof(left, right) { let proto = Object.getPrototypeOf(left), // 获取对象的原型 prototype = right.prototype; // 获取构造函数的 prototype 对象
// 判断构造函数的 prototype 对象是否在对象的原型链上 while (true) { if (!proto) return false; if (proto === prototype) return true;
proto = Object.getPrototypeOf(proto);
} }
详细资料可以参考: [《instanceof》](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/instanceof)
<a name="B1HlD"></a>
### new 操作符具体做了些什么?怎么实现
1. 首先**创建**了一个新的**空对象**
1. 设置原型,将**对象的原型**设置为**函数的 prototype** 对象。
1. 让函数的 this 指向这个对象,**执行构造函数的代码**(为这个新对象添加属性)
1. **判断**函数的**返回值类型**,如果是**值类型**,**返回创建的对象**。如果是**引用类型**,就**返回这个引用类型**的对象。
```javascript
function objFactory (){
let newObj = null;
let constructor = Array.prototype.shift.call(arguments);
let result = null;
if(typeof constructor !== "function"){
throw Error("constructor is not function")
}
newObj = Object.create(constructor.prototype)
result = constructor.apply(newObj,argumnets)
let flag = result && (typeof result === "object" || typeof result === "function");
return flag ? result : newObj;
}
// 使用方法
objFactory(构造函数,参数)
详细资料可以参考: 《new 操作符具体干了什么?》 《JavaScript 深入之 new 的模拟实现》