JavaScript Object 对象 再解(五)
JavaScript 中的对象分类
我们可以把对象分为以下的几类。
-
宿主对象(host Objects):由JavaScript 宿主环境提供的对象,它们的行为完全由宿主环境决定。
宿主对象千奇百怪,前端最熟悉的就是浏览器环境里面的宿主了。在浏览器里面,我们都知道全局对象是 window,window 上又有很多的属性,比如说 document。
实际上,这个全局对象 window 上的属性,一部分来自 JavaScript 语言,还有一部分来自浏览器环境。
JavaScript 标准中规定了全局对象属性,w3c 的各种标准规定了 window 对象的其它属性。
宿主对象也分为固有的和用户可创建的两种,比如 document.createElement 就可以创建一些 DOM 对象。
宿主浏览器也会提供一些构造器,比如我们可以使用 new Image 来创建 img 元素。
-
内置对象(Built-in Objects):由 JavaScript 语言提供的对象。
-
固有对象(Intrinsic Objects):由标准规定,随着 JavaScript 运行时创建而自动创建的对象实例。
固有对象时标准制定,随着 JavaScript 运行时创建而自动创建。
固有对象在任何 JS 代码执行之前就已经被创建出来了,它们通常扮演着基础库的角色。我们前面提到的“类”其实就是固有对象的一种。
ECMA 标准提供的固有150+固有对象,但是不完整,完整的看文章结尾。
-
原生对象(Native Objects):可以由用户通过 Array、RegExp 等内置构造器或者特殊语法创建的对象。
JavaScript 中,能够通过语言本身的构造器创建的对象称为原生对象。在 JavaScript 中,提供了 30 多个构造器。下面按照 winter 老师的理解,按照不同应用场景,把原生对象分为了一下几个种类。
通过这些构造器,我们就可以通过 new 运算来创建新的对象,所以我们把这些对象称为原生对象。
几乎所有这些构造器的能力都是无法用纯 JavaScript 代码实现的,它们也无法用 class、extend 语法来继承。
这些构造器创建的对象大多数都是用了私有字段,例如:
- Error: [[ErrorData]]
- Boolean: [[BooleanData]]
- Number: [[NumberData]]
- Date: [[DateValue]]
- RegExp: [[RegExpMatcher]]
- Symbol: [[SymbolData]]
- Map: [[MapData]]
这些字段🙆原型继承方法无法正常工作,所以,我们认为,所有这些原生对象都是为了特定的能力,而设计出来的“特权对象”
-
普通对象(Ordinary Objects):由{}语法、Object 构造器或者 class 关键字定义类创建的对象,它能够被原型继承。
普通对象还包含函数对象与构造器对象。
-
函数对象:具有[[call]]私有字段的对象。
JavaScript 用对象模拟函数的设计代替了一般编程语言中的函数,它们可以像其它函数一样被调用、传参。任何宿主只要提供了“具有[[call]]私有字段的对象”,就可以被 JavaScript 函数调用语法支持。
[[call]]私有字段必须是一个引擎中定义的函数,需要接受 this 值和调用参数,并且会产生域的切换。
我们可以这样说,任何对象只要实现 [[call]],它就是一个函数对象,可以去作为函数被调用。用户通过 function 创建的函数必定是函数和构造器。
-
构造器对象:具有私有字段[[construct]]的对象
如果一段代码能够实现[[construct]],那么它就是构造器对象。
队友宿主和内置对象来说,它们实现[[call]](作为函数被调用) 和 [[construct]](作为构造器被调用)时产生的行为不总是一致的。比如内置对象 Date 在作为构造器在其调用时产生新的对象,作为函数时则产生字符串。
// Wed Jun 17 2020 09:42:20 GMT+0800 (中国标准时间) console.log(new Date); // "Wed Jun 17 2020 09:42:42 GMT+0800 (中国标准时间)" console.log(Date())
然而浏览器宿主环境里面提供的 Image 构造器,根本不允许作为函数调用。
// <img> console.log(new Image) // TypeError: ..........this DOM object constructor cannot be called as a function Image()
在比如基本类型(String、Number、Boolean),它们的构造器被当做函数调用,则就会产生类型转换的效果。
值得一提的是,在 ES6 之后 => 语法创建的函数就仅仅是函数,无法被当作构造器使用,见一下代码:
// error new (a => 0)
对于用户使用 function 语法或者 Function 构造器创建的对象来说,[[call]]与[[construct]]行为总是相似的,它们执行同一段代码。
例如:
function f() { return 1; } var v = f(); var o = new f();
我们大致可以认为,它们[[construct]]的执行过程如下:
- 以 Object.prototype为原型创建一个新对象
- 以新对象为 this,执行函数的[[call]]
- 如果[[call]]的返回值是一个对象,那么返回这个对象,否则返回第一步创建的新对象
这样的规则造成了一个有趣的现象,如果我们的构造器返回了一个新的对象,那么 new 创建的新对象就变成了一个构造函数之外完全无法访问的对象,这可以在一定程度上实现私有。
function cls() { this.a = 100; return { // getValue作为对象的属性被返回出去, // 参考第三步,[[call]] 的返回值是一个对象,返回了这个对象,所以没有返回一开始创建的新对象 getValue: () => this.a } } var o = new cls; // error cls.a // correct els.getValue()
-
-
特殊行为对象
除了上面介绍的对象之外,在固有对象和原生对象里面,还有一些对象的行为与正常的对象的行为有很大的区别。
它们常见的下标运算(就是使用object[a]或者object.a 中括号或者点来做属性访问)或者设置原型跟普通的对象不同,这里我们简单的总结一下。
-
Array:Array 的 length 属性会根据最大的下标发生自动变化。
-
Object.prototype:作为所有正常对象的默认原型,不能再给它设置原型了。
-
String: 为了支持下标运算,String 的正整数访问会去字符串里面查找。
-
Arguments:arguments 的非负整数型下标属性跟对应的变量联动。
function f(a,b,c,d,e,f,g) { console.log(arguments); } // Arguments(7) [1, 2, 3, 4, 5, 6, 7, callee: ƒ, Symbol(Symbol.iterator): ƒ] function(1,2,3,4,5,6,7)
-
模块化的 namespace 对象:特殊的地方非常多,跟一般对象完全不一样,建议不使用,用了会被打系列,还是尽量用 import。
-
类型数组和数组缓冲区:跟内存块相关联,下标运算比较特殊。
-
bind 后的 function:跟原来的函数相关联。
-
-
-
作业
-
小实验:不适用 new 运算符,尽可能找到获得对象的方法。
例子:
var o = {};
var o = function(){}
答案:
// 数组
var a = [];
// 这个好像不是对象,是 string
//var b = '';
// 字面量
var c = {};
// 正则
var d = /abcd/gi;
// dom api
var e = document.createElement('a');
// js Object 内置方法
var f = Object.create(null);
// js Object内置方法
var g = Object.assign({{});
// JSON转换
var h = JSON.parse('{}');
// 装箱转换
var i = Object(null);
var j = Object(undefined);
var k = Object(1)
var l = Object('abc')
var m = Object(true)
// 构造函数
function n() {
this.name = '';
this.age = '';
this.sayHello = () => console.log('hello');
}
-
获取全部 JavaScript 的固有对象
我们从 JavaScript 可以找到全部的 JavaScript 对象定义。JavaScript 语言规定了全局对象的属性。
三个值:
Infinity、NaN、undefined
九个函数:
- eval
- isFinite
- isNaN
- parseFloat
- parseInt
- decodeURI
- decodeURIComponent
- encodeURI
- encodeURIComponent
构造器:
- Array
- Date
- RegExp
- Promise
- Proxy
- Map
- WeakMap
- Set
- WeakSet
- Function
- Boolean
- String
- Number
- Symbol
- Object
- Error
- EvalError
- RangeError
- ReferenceError
- SyntaxError
- TypeError
- URIError
- ArrayBuffer
- SharedArrayBuffer
- DataView
- Typed Array
- Float32Array
- Float64Array
- Int8Array
- Int16Array
- int32Array
- UInt8Array
- UInt16Array
- UInt32Array
- UInt8ClampedArray
四个用于当做命名空间的对象:
- Atomics
- JSON
- Math
- Reflect
我们使用广度优先搜索(自己算法还是薄弱呀,学完重学前端与前端进阶训练营要好好干干算法),在自己的浏览器里面计算出来JavaScript 有多少固有对象。
答案:
var set = new Set();
var objects = [
eval,
isFinite,
isNaN,
parseFloat,
parseInt,
decodeURI,
decodeURIComponent,
encodeURI,
encodeURIComponent,
Array,
Date,
RegExp,
Promise,
Proxy,
Map,
WeakMap,
Set,
WeakSet,
Function,
Boolean,
String,
Number,
Symbol,
Object,
Error,
EvalError,
RangeError,
ReferenceError,
SyntaxError,
TypeError,
URIError,
ArrayBuffer,
SharedArrayBuffer,
DataView,
Float32Array,
Float64Array,
Int8Array,
Int16Array,
Int32Array,
Uint8Array,
Uint16Array,
Uint32Array,
Uint8ClampedArray,
Atomics,
JSON,
Math,
Reflect,
];
objects.forEach((item) => set.add(item));
for (let i = 0; i < objects.length; i++) {
var o = objects[i];
for (var p of Object.getOwnPropertyNames(o)) {
var d = Object.getOwnPropertyDescriptor(o, p);
if (
(d.value !== null && typeof d.value === 'object') ||
typeof d.value === 'function'
) {
if (!set.has(d.value)) {
set.add(d.value), objects.push(d.value);
}
if (d.get) {
if (!set.has(d.get)) {
set.add(d.get), objects.push(d.get);
}
}
if (d.set) {
if (!set.has(d.set)) {
set.add(d.set), objects.push(d.set);
}
}
}
}
}
console.log(set);
console.log('/////////////////////////////////');
console.log('**********这里是测试片段**********');
console.log(objects);