6

系列文章

1 、js数据类型--object

一、数据类型

  在JavaScript中,数据类型可以分为原始类型以及引用类型。其中原始类型包括string,number, boolean, null, undefined, symbol(ES6新增,表示独一无二的值),这6种数据类型是按照值进行分配的,是存放在栈(stack)内存中的简单数据段,可以直接访问,数据大小确定,内存空间大小可以分配。引用类型包括function,object,array等可以可以使用new创建的数据,又叫对象类型,他们是存放在堆(heap)内存中的数据,如var a = {},变量a实际保存的是一个指针,这个指针指向对内存中的数据 {}

传送门:更多symbol的用法可以看阮一峰ECMAScript 6 入门

  讲到数据,那不得不讲的就是变量,JavaScript中的变量具有动态类型这一特性,这意味着相同的变量可用作不同的类型:

var x;            // x 为 undefined
x = 6;            // x 为 number
x = "hfhan";      // x 为 string

  JavaScript中可以用typeof 操作符来检测一个数据的数据类型,但是需要注意的是typeof null结果是object, 这是个历史遗留bug:

typeof 123;               // "number"
typeof "hfhan";           // "string"
typeof true;              // "boolean"
typeof null;              // "object"  独一份的与众不同
typeof undefined;         // "undefined"
typeof Symbol("hfhan");   // "symbol"
typeof function(){};      // "function"
typeof {};                // "object"

二、对象类型

先理解下什么是宿主环境:由web浏览器或是桌面应用系统造就的js引擎执行的环境即宿主环境。

1、本地对象

  ECMA-262 把本地对象(native object)定义为“独立于宿主环境的 ECMAScript 实现提供的对象”。

  本地对象包含但不限于Object、Function、Array、String、Boolean、Number、Date、RegExp、各种错误类对象(Error、EvalError、RangeError、ReferenceError、SyntaxError、TypeError、URIError)

  注意:这里的Object、Function、Array等不是指构造函数,而是指对象的类型

2、内置对象

  ECMA-262 把内置对象(built-in object)定义为“由 ECMAScript 实现提供的、独立于宿主环境的所有对象,在 ECMAScript 程序开始执行时出现”。这意味着开发者不必明确实例化内置对象,它已被实例化了。ECMA-262 只定义了两个内置对象,即 Global 和 Math (它们也是本地对象,根据定义,每个内置对象都是本地对象)。

  其中Global对象是ECMAScript中最特别的对象,因为实际上它根本不存在,但大家要清楚,在ECMAScript中,不存在独立的函数,所有函数都必须是某个对象的方法。类似于isNaN()、parseInt()和parseFloat()方法等,看起来都是函数,而实际上,它们都是Global对象的方法。而且Global对象的方法还不止这些。有关Global对象的具体方法和属性,感兴趣的同学可以看一下这里:JavaScript 全局对象参考手册

  对于web浏览器而言,Global有一个代言人window,但是window并不是ECMAScripta规定的内置对象,因为window对象是相对于web浏览器而言的,而js不仅仅可以用在浏览器中。

  Global与window的关系可以看这里:概念区分:JavaScript中的global对象,window对象以及document对象

  可以看出,JavaScript中真正的内置对象其实只有两个:Global 和 Math,可是观看网上的文章资料,千篇一律的都在讲JavaScript的11大内置对象(不是说只有11个,而是常用的有11个:Object、Function、Array、String、Boolean、Number、Date、RegExp、Error、Math、Global,ES6中出现的Set 、Map、Promise、Proxy等应该也算是比较常用的),这是不严谨的,JavaScript中本地对象、内置对象和宿主对象一文中,把本地对象、内置对象统称为“内部对象”,算是比较贴切的。

  更多“内部对象”可以查看MDN>JavaScript>引用>内置对象 内容,或者通过浏览器控制台打印window来查找。

3、宿主对象

由ECMAScript实现的宿主环境提供的对象,可以理解为:浏览器提供的对象。所有的BOM和DOM都是宿主对象。

4、自定义对象

顾名思义,就是开发人员自己定义的对象。JavaScrip允许使用自定义对象,使JavaScript应用及功能得到扩充

5、判断对象的类型

对象的类型不能使用typeof来判断,因为除了Function外其他类型的对象所得到的结果全为"object"

typeof function(){};      // "function"
typeof {};                // "object"
typeof new RegExp;        // "object"
typeof new Date;          // "object"
typeof Math;              // "object"
typeof new Error;         // "object"
…

一个使用最多的检测对象类型的方法是 Object.prototype.toString

Object.prototype.toString.apply(new Function);     // "[object Function]"
Object.prototype.toString.apply(new Object);       // "[object Object]"
Object.prototype.toString.apply(new Date);         // "[object Date]"
Object.prototype.toString.apply(new Array);        // "[object Array]"
Object.prototype.toString.apply(new RegExp);       // "[object RegExp]"
Object.prototype.toString.apply(new ArrayBuffer);  // "[object ArrayBuffer]"
Object.prototype.toString.apply(Math);             // "[object Math]"
Object.prototype.toString.apply(JSON);             // "[object JSON]"
var promise = new Promise(function(resolve, reject) {
    resolve();
});
Object.prototype.toString.apply(promise);          // "[object Promise]"
…

三、构造函数

构造函数是描述一类对象统一结构的函数——相当于图纸

1、对象的创建

  上面我们已经知道了,JavaScript中的对象有很对种类型,比如Function、Object、Array、Date、Set等等,那么我们如何去创建这些类型的数据?

生成一个函数可以通过function关键字:

function a(){
    console.log(1)
}
//或者
var b = function(){
    console.log(2)
}

  此外创建一个对象(类型为Object的对象),可以通过{};创建一个数组,可以通过[];创建一个正则对象可以通过/.*/。但是那些没有特殊技巧的对象,就只能老老实实使用构造函数来创建了。

  JavaScript 语言中,生成实例对象的传统方法是通过构造函数,即我们通过函数来创建对象,这也证明了函数在JavaScript中具有非常重要的地位,因此说函数是一等公民。

2、构造函数创建对象

  JavaScript中的对象在使用的时候,大部分都需要先进行实例化(除了已经实例化完成的Math对象以及JSON对象):

var a = new Function("console.log('a') ");  //构造函数创建Function对象
var b = new Object({a:1});                  //构造函数创建Object对象
var c = new Date();                         //构造函数创建Date对象
var d = new Set();                          //构造函数创建Set对象
var e = new Array(10);                      //构造一个初始长度为10的数组对象

  可以看出,只要使用new关键字来实例化一个构造函数就可以创建一个对象了,JavaScript中内部对象的构造函数是浏览器已经封装好的,我们可以直接拿过来使用。

使用构造函数创建的数据全是对象,即使用new关键字创建的数据全是对象,其中new做了4件事:

1)、先创建空对象
2)、用空对象调用构造函数,this指向正在创建的空对象 
    按照构造函数的定义,为空对象添加属性和方法
3)、将新创建对象的__proto__属性指向构造函数的prototype对象。
4)、将新创建对象的地址,保存到等号左边的变量中
      

除了浏览器本身自带的构造函数,我们还可以使用一个普通的函数来创建对象:

function Person(){};
var p1 = new Person()

  这个例子中Person就是一个普普通通的空函数,但是他依然可以作为构造函数来创建对象,我们打印下p1的类型,可以看出使用自定义的构造函数,所创建的对象类型为 Object

Object.prototype.toString.apply(p1);  // "[object Object]"

3、构造函数和普通函数

  实际上并不存在创建构造函数的特殊语法,其与普通函数唯一的区别在于调用方法。对于任意函数,使用new操作符调用,那么它就是构造函数,又叫工厂函数;不使用new操作符调用,那么它就是普通函数。

  按照惯例,我们约定构造函数名以大写字母开头,普通函数以小写字母开头,这样有利于显性区分二者。例如上面的new Object (),new Person ()。

四、原型与原型链

1、prototype 与 __proto

原型是指原型对象,原型对象从哪里来?

  每个函数在被创建的时候,会同时在内存中创建一个空对象,每个函数都有一个prototype 属性,这个属性指向这个空对象,那么这个空对象就叫做函数的原型对象,而每一个原型对象中都会有一个constructor属性,指向该函数

function b(){console.log(1)};
b.prototype.constructor === b;   // true

抽象理解:构造函数是妻子,原型对象是丈夫,prototype是找丈夫,constructor是找妻子。

手动更改函数的原型对象:

var a = {a:1};
b. prototype = a;  //更改b的原型对象为a
a. constructor;    // function Object() { [native code] }

为什么这里a. constructor不指向b函数?

  这是因为变量a所对应的对象是事先声明好的,不是跟随函数一起创建的,所以他没有constructor属性,这时候寻找constructor属性就会到父对象上去找,而所有对象默认都继承自Object. Prototype,所以最后找的就是Object. Prototype. Constructor,也就是Object函数。

刚才讲到了继承,继承又是怎么一回事呢?

所有对象都有一个__proto__ 属性,这个属性指向其父元素,也就是所继承的对象,一般为构造函数的prototype对象。

prototype 与 __proto

  prototype 是函数独有的;__proto__ 是所有对象都有的,是继承的。调用一个对象的某一属性,如果该对象上没有该属性,就会去其原型链上找。

  比如上例中,调用p.a,对象p上找不到a属性,就会去找p.__proto__.a,p.__proto__.a也找不到,就会去找p.__proto__.__proto__.a,依次类推,直到找到Object.prototype.a也没找到,就会返回undefined。

  原型链是由各级子对象的__proto__属性连续引用形成的结构,所有对象原型链的顶部都是Object.prototype。

  我们知道,当子对象被实例化之后再去修改构造函数的prototype属性是不会改变子对象与原型对象的继承关系的,但是通过修改子对象的__proto__属性,我们可以解除子对象与原型对象之间的继承关系。

var A = function(){};    // 构造函数
A.prototype = {a:1};     // 修改原型对象
var a = new A;           // 实例化子对象a,此时a继承自{a:1}
a.a                      // 1
A.prototype = {a:2}      // 更该构造函数的原型对象
a.a                      // 1   此时,a仍是继承自{a:1}
a.__proto__ = {a:3}      // 修改a的原型链
a.a                      // 3   此时,a继承自{a:3}

2、Object.prototype与Function.prototype

  一切诞生于虚无!

  上面讲了,所有对象原型链的顶部都是Object.prototype,那么Object.prototype是怎么来的,凭空造的吗?还真是!

Object.prototype.__proto__ === null;   // true

  上面讲了,我们可以通过修改对象的__proto__属性来更改继承关系,但是,Object.prototype的__proto__属性不允许更改,这是浏览器对Object.prototype的保护措施,修改Object.prototype的__proto__属性会抛出错误。同时,Object.prototype.__proto__也只能进行取值操作,因为null 和 underfined没有对应的包装类型,因此不能调用任何方法及属性

  在控制台打印下Object.prototype.__proto__的保护属性:

Object.getOwnPropertyDescriptor(Object.prototype,"__proto__"); 

注:保护属性及getOwnPropertyDescriptor为ES5中内容。

1

  可以看到,其numerable、configurable属性均为false,也就是Object.prototype.__proto__属性不可删除,不可修改属性特性,并且属性做了get、set的处理。

  Object.prototype与Function.prototype是原型链中最难理解也是最重要的两个对象。下面我们用抽象的方法来理解这两个对象:

  天地伊始,万物初开,诞生了一个对象,不知其姓名,只知道他的类型为"[object Object]",他是一切对象的先祖,为初代对象,继承于虚无(null)。

  后来,又诞生了一个对象,也不知其姓名,只知道他的类型为"[object Function]",他是一切函数的先祖,继承于对象先祖,为二代对象。

Object.prototype与Function.prototype

  经年流转,函数先祖发挥特长,制造出了一系列的函数,如Object、Function、Array、Date、String、Number等,都说龙生九子各有不同,这些函数虽说各个都貌美如花,神通通天,但功能上还是有很大的区别的。

  其中最需要关注的是Object以及Function。原来函数先祖在创造Function的时候,悄悄的把Function的prototype属性指向了自己,也把自己的constructor属性指向了Function。如果说Function是函数先祖为自己创造的妻子,那么Object就是函数先祖为对象先祖创造的妻子,同样的,Object的prototype属性指向了对象先祖,对象先祖也把自己的constructor属性指向Object,表示他同意了这门婚事。

  此后,世人都称对象先祖为Object.prototype,函数先祖为Function.prototype。

  从上可以看出,对象先祖是一开始就存在的,而不是同Object一起被创建的,所以手动更改Object.prototype的指向后:

Object.prototype = {a:1};    //修改Object.prototype的指向
var a = {};                  //通过字面量创建对象
a.a                          //undefined 此时a仍然继承于对象先祖
var b = new Object();        //通过new来创建对象
b.a                          //结果是???

  这里我原本以为会打印1,但是实际上打印的还是undefined,然后在控制台打印下Object.prototype,发现Object.prototype仍然指向对象先祖,也就是说Object.prototype = {a:1}指向更改失败,我猜测和上面Object.prototype的__proto__属性不允许更改,原因是一样的,是浏览器对Object.prototype的保护措施。

  在控制台打印下Object.prototype的保护属性:

Object.getOwnPropertyDescriptor(Object,"prototype"); 

2

  可以看到,其writable、enumerable、configurable属性均为false,也就是其prototype属性不可修改,不可删除,不可修改属性特性。

  其实不光Object.prototype不能修改,Function. Prototype、String. Prototype等内部对象都不允许修。

我们继续往下看

3

因为Object、Function、Array、String等都继承自Function.prototype,所以有

Object.__proto__ === Function.prototype;       // true
Function.__proto__ === Function.prototype;     // true
Array.__proto__ === Function.prototype;        // true
String.__proto__ === Function.prototype;       // true

所有的对象都继承于Object.prototype,所以有

Function.prototype.__proto__ === Object.prototype;     // true
Array.prototype.__proto__ === Object.prototype;        // true
String.prototype.__proto__ === Object.prototype;       // true

3、自定义构造函数创建对象

  当我们自定义一个对象的时候,这个对象在整个原型链上的位置是怎么样的呢?

  这里我们不对对象的创建方式多做讨论,仅以构造函数为例

  当我们使用字面量创建一个对象的时候,其父对象默认为对象先祖,也就是Object.prototype

var a = {};
a.__proto__ === Object.prototype;  // true

  上面讲了,自定义构造函数所创建的对象他的类型均为"[object Object]",在函数建立的时候,会在内存中同步建立一个空对象,其过程可以看作:

function F(){};  // prototype 赋值  F.prototype = {},此时{}继承于Object.prototype

  当我们使用构造函数创建一个对象时,会把构造函数的prototype属性赋值给子对象的__proto__属性,即:

var a = new F();   //__proto__赋值 a.__proto__ = F.prototype;

  因为F.prototype继承于Object.prototype,所以有

a.__proto__.__proto__ === Object.prototype;  // true

z

  综上我们可以看出,原型链就是根据__proto__维系的由子对象-父对象的一条单向通道,不过要理解这条通道,我们还需要理解构造对象,类,prototype,constructor等,这些都是原型链上的美丽的风景。

  最后希望大家可以在javascript的大道上肆意驰骋。

其他好文

JavaScript 世界万物诞生记
Prototype 与 Proto 的爱恨情仇


hfhan
28.9k 声望27.4k 粉丝

砥砺前行


引用和评论

0 条评论