一、数据类型
在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__
是所有对象都有的,是继承的。调用一个对象的某一属性,如果该对象上没有该属性,就会去其原型链上找。
比如上例中,调用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中内容。
可以看到,其numerable、configurable属性均为false,也就是Object.prototype.__proto__
属性不可删除,不可修改属性特性,并且属性做了get、set的处理。
Object.prototype与Function.prototype是原型链中最难理解也是最重要的两个对象。下面我们用抽象的方法来理解这两个对象:
天地伊始,万物初开,诞生了一个对象,不知其姓名,只知道他的类型为”[object Object]”,他是一切对象的先祖,为初代对象,继承于虚无(null)。
后来,又诞生了一个对象,也不知其姓名,只知道他的类型为”[object Function]”,他是一切函数的先祖,继承于对象先祖,为二代对象。
经年流转,函数先祖发挥特长,制造出了一系列的函数,如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");
可以看到,其writable、enumerable、configurable属性均为false,也就是其prototype属性不可修改,不可删除,不可修改属性特性。
其实不光Object.prototype不能修改,Function. Prototype、String. Prototype等内部对象都不允许修。
我们继续往下看
因为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
综上我们可以看出,原型链就是根据__proto__
维系的由子对象-父对象的一条单向通道,不过要理解这条通道,我们还需要理解构造对象,类,prototype,constructor等,这些都是原型链上的美丽的风景。
最后希望大家可以在javascript的大道上肆意驰骋。