JavaScript中有七种数据类型,分别是number、string、boolean、symbol、undefined、null、object。我们所见到的数组以及函数等,并不是独立的一种数据类型,而是包含在object里的。
数字类型。
1. 十进制。表示数可以有’1’,’1.1’,’1.11e2’(科学计数法表示111)以及’.1’(0.1的特殊写法)。
2. 二进制。以0b开头,比如0b11(等于十进制3)。
3. 八进制。以0开头,比如011(等于十进制9)。这里容易出现bug,如果一个十进制数字以0开头,并且后边的数字全部小于8,js会自动将其理解为八进制数,由此会出现一些意料之外的结果。比如说如果你将电话号码存为了number类型,var phone = 0102456547
,输出phone得到的结果会是17456487。
4. 十六进制。以0x开头,比如011(等于十进制17)。
字符串类型。用单引号或者双引号都可以,一般用单引号因为css用双引号。里边什么都不写是空字符串,区分里边有一个空格的空格字符串,两个字符串长度不一样。一个是0一个是1。
如果你的字符串里有单引号双引号等特殊字符的话,使用转义符’\’进行字符的转义,使其能够正常显示。可以转义自身。
转义符除了可以用来显示特殊字符,还可以用\n表示回车,\t表示制表符。看上去虽然有两个字符但是长度为1。
多行字符串(字符串里不存在回车,但是由于它太长了为了方便阅读我们需要给它人为的加上一个回车)。但是如果直接加回车的话会报错,所以我们需要用别的方法。命令行中输入回车会直接执行命令,但是如果加反斜杠’\’就可以换行输入。在js里边,这也行得通。但是这样做有一个很大的弊端。如果你不小心在反斜杠后边加了一个空格,同样会报错,并且这样的错误很难觉察。或者你可以用’+’进行字符串的连接操作来达成目的,虽然麻烦了一些但是可以避免不必要的麻烦。如果你对字符串的长度之类的东西无所谓的话还有一个方便并且不会报错的方法,使用'``'
反引号(英文输入状态下按数字键1左边的那个键可以显示出来)可以直接在字符串中间加回车,但是这个回车会算作一个字符保存在字符串里边。
总的来说表示多行字符串有三种方法:
1. ‘\’,不推荐使用,容易出bug
2. ‘+’,字符串连接,麻烦但是稳妥
3. ‘“’,用反引号代替引号,将回车算作字符串的一部分保存,方便
布尔类型。只有两个值,true和false。js里边的布尔运算主要是&&(and)和||(or)。对于A&&B
只有A和B都为真的时候值才为真,对于A||B
,A和B只要有一个为真值就为真。
这里学过其它语言的注意一下,我记得Java里边好像这个称为“短路与”和“短路或”,意思比如说对于A&&B
如果A为假那么B将不再被执行,而对于A&B
,即使A为假B也会执行判断。但是在js中,如果你使用的是&
运算符,它会把两边的东西统一转换为二进制,然后进行逐位比较,最后得出结果。
symbol用来生成一个全局唯一的值,但并不是字符串。
var sym1 = Symbol()
var sym2 = Symbol('discription') //里边的只是对这个值得描述,与这个值本身无关。
这两个或许放到一起比较合适,因为实际上表示的就是同一个东西——什么都没有。
区别:
1. 如果变量没有赋值,那么就是undefined。这是一个语法。
2. 有一个对象,但是现在还不想赋值,那就给一个null(这是推荐值,也可以给undefined)。有一个非对象(比如说number,string之类的),不想给值,推荐初始化为undefined。由于不赋值的话默认就是undefined,所以直接var n
就可以了,此时n的值就是undefined。这只是一个惯例。
一般来说null表示空对象,undefined表示空的其他东西,比如number,string等。
上边的都是基本类型,也叫简单类型,object是复杂类型。复杂类型是由简单类型组成的。
定义的时候直接用hash表,左边一定是字符串,即使你没有加引号也默认是字符串。右边如果是字符串的话一定要加引号不然默认是变量,同时如果这个变量没有定义的话会报错。取的时候可以用 .
或者是 []
,比如person.age
或者person['age']
。里边的引号不能省,不然指的是变量,而如果那个变量没有定义的话会报错,如果定义了并且变量值不等于变量名的话程序功能会出错。基本语法如下
var obj = {
'a': 'stringaaa',
'b': 123,
'x': true
}
左边的key可以为空,可以是空格可以是任何东西只要加了引号。如果没有加引号的话就必须符合标识符的命名规范,并且只有符合标识符命名规范的key才可以通过.
运算符读取。
右边的value除了基本数据类型之外还可以是另一个对象,也就是说,对象可以嵌套对象。并且还可以嵌套自身。
创建对象的三种方法:
1. var obj1 = {}
2. var obj2 = new Object()
3. var obj3 = Object.create(Object.prototype)
delete person.name
用来删除一个key,这个时候去访问person.name的话得到的是undefined。此时这个key已经不在person里边了。即'name' in person
结果为false。区别于person.name = undefined
。后者只是清空了value,前者把key和value一起清了。
for(var key in person){
console.log(key,person[key]) //遍历person对象中的key和value并输出
}
typeof xxx
查看变量xxx的数据类型。一般都能返回一个正常的结果,比如
typeof 1 //"number"
typeof true //"boolean"
typeof 'xxx' //"string"
typeof {} //"object"
typeof [] //"object"
但是有一些例外(bug),比如
typeof null //"object"
typeof function(){} //"function"
第一个强行解释的话大概是因为null一般用来表示一个空的对象,所以返回object。
xxx.toString()
"[object Object]"
无法变成你想要的字符串。JSON.Stringify(xxx)
可以将对象转成正常的字符串。xxx+''
null + '' //"null"
undefined + '' //"undefined"
window.String(xxx)
+''
一样Boolean(xxx)
!!xxx
Number('1')
parseInt('1',10)
,parse:解析。 paresInt('011')
得到的结果会是11
,它会把011当做一个十进制数。parseInt('11cy') //11
parseFloat('1.23')
字符串转为浮点数。'1'-0
任何东西减0都是number+'1'
对一个数取正也可以。这里注意一点,“取正”,或许这样才对。因为它并没有取正,对于+'-1'
来讲,它的结果仍然是-1
假设你不知道数据在内存中的存储位置有堆内存(Heap)和栈内存(Stack),那么,让我们设想一下,你有一堆数据,我们需要将他们有序的存到内存中。
var a = 1;
var b = 2;
var obj = {
name: 'wcy'
}
var c = 3;
我们按照先后顺序执行上述代码,将这些数据一个个存到内存里。但是问题来了,现在我们已经存好了,但是我加了一句代码obj.age = 18
,so?你想到了什么?我们刚刚把他们全都,按顺序存好了。如果要加属性的话必定要占用更大一点的内存空间来存放它。那么,我们需要把变量c的存储位置往后挪。如果后边多来些变量,如果频繁加obj的属性?这样未免太低效了吧!
所以聪明的程序员们发明了Stack,用来存对象的地址,以及其它的基本类型(除开string,因为我还没搞明白)。而将对象的值,放在Heap里边,Heap里边不按顺序存储。这样我们可以将上述代码画一个内存图如下:
万变不离其宗,对于a = b
这类问题的所有答案都是Stack中的值的替换。
对于上述代码中,如果我们后边加一句b = a
,那么输出b的值就是1,因为a的值存在Stack中。
而如果我们加上如下代码
var obj2 = obj
obj2.name = '2333'
console.log(obj.name) //"2333"
obj2 = {
name: '666'
}
console.log(obj.name) //"2333"
上边代码运行的结果不能理解的可以看这里,可以理解那就不用看。
第一句我们将obj在Stack里边的内容拷贝给了obj2,所以obj2里边存的实际上是{name: ‘wcy’}的地址53,所以obj2.name = '2333'
实际上是将addr53里边的name改成了’2333’,因此obj.name的结果是2333。
第二种则是先在Heap里边创建一个对象,然后再将对象的地址赋值给obj2在Stack里边的值,所以对obj1没有影响。
等于号永远只做一件事情,把等号右边的东西存到左边。不论右边是值还是地址。
区分:
var a = {}
a.self=a
这个里边self的值是a的地址,做成了一个循环,可以无限.self
。
var a = {self: a}
这个里边由于变量提升,相当于
var a //a = undefined
a = {self: a}
self的值是undefined。
如果一个对象没有被引用,那么它就是垃圾,将被回收。垃圾回收的时间随机,在浏览器觉得有需要的时候就会进行回收。
对于以下代码:
var fn = function(){}
document.body.onclick = fn
fn = null
此时function(){}并不是垃圾,因为document.body.onclick仍然在引用它。在内存图中的体现如下
但是如果页面关了,此时document对象不存在所以对body的引用也不存在,那么此时body.onclick.function(){}这条引用与Stack没了联系,所以理论上来讲这一堆就是垃圾,将会被浏览器回收。但是IE6有bug,它认为这个不是垃圾。如果只是关掉这个标签页并不能起作用。解决方法是把你所有的这个事件都手动清掉,不然会导致内存泄漏。
window.onunload = function(){
document.body.onclick = null //所有的点击事件都要清掉,所以最好是在绑定的时候就加上这个。不然需要一个一个找
}
深拷贝:对b做的任何改动都不影响a,基本类型的赋值就是深拷贝
浅拷贝:b变了a也会变,比如正常的复杂类型(object)的赋值
object的深拷贝:把a里边的所有东西复制一份放到内存中,然后把复制品的地址给b
浏览器默认的全局对象是window,在ECMAScript标准里边这个全局变量叫做global,window是一个特例。
window的属性包括有ECMAScript规定的和浏览器独有的。其中ECMAScript规定的包括比如parseInt()
,parseFloat()
,Object()
,Number()
,Boolean()
,String()
,Symbol()
等。浏览器独有的属性是各浏览器自己实现的,没有一个统一的标准,所以在效果方面可能会有所不同。浏览器独有的属性包括有alert
(弹框提示),prompt
(用户填写),confirm
(确认信息),console
(开发者使用),document
(文档),history
。
其中浏览器属性中的document有它自己的规范——DOM。DOM的规范由W3C规定。document代表了你能对文档所做的一切操作。document是js的一个对象,js的规范由ECMAScript规定。
history的规范有一个名字——BOM。
var a = new Number(1)
,这样会创建一个对象,对象里边的一个键值对是{valueOf(): 1,}
,因此用a.valueOf()
可以获取到这个原始值。 var n = 1
这种声明方式,JavaScript也为我们提供了对这个数值的各种便捷的操作。所以为什么会有这么诡异的声明方式呢?因为Branden Eich的Boss说JavaScript必须要长得像Java。这样子长得才像Javavar n = new Number()
。var n = 1
这种声明方式只是在栈内存中开辟了一段空间用来存储1这个数值。那么它又是怎么实现对象的复杂方法的呢?JavaScript内部为其进行了一个临时转换。在我们写下n.toString()
的时候,相当于temp = new Nember(n); return temp.toString();
得到返回值之后丢弃temp。这里注意temp只是一个临时的转换,在操作完n.toString()这句话之后这个变量就被丢弃了。因此如果你写var n = 1; n.xxx = 2;
的时候不会给你报错。它创建了一个临时的对象来完成这个赋值操作,但是你无法读取n.xxx,因为这个临时对象完成使命之后就被丢弃了。在这样的机制下,用new来声明number类型的值就成了一种极其愚蠢的做法了,因为它除了增加代码长度以外,实在没有半点实质性的好处。var s = 'hello world'
var s = new String('hello world')
s.charAt(2)
表示获取某一个索引对应的字符,相当于s[2]
。s.charCodeAt(2)
获取这个字符对应的Unicode编码,与toString(16)搭配使用'a'.charCodeAt().toString(16)
可以得到一个字符的16进制Unicode编码。s.trim()
。s1.concat(s2)
返回的是字符串s1和s2连接之后的结果(concat与命令行有关。在命令行中cat用于打印出文件内容。所以concat的含义是把两个文件内容连接起来然后打印出来)。 s.slice(0,2)
表示切片,把字符串从第0个到第二个字符打印出来(包括0不包括2)。s.replace('e','o')
,把e变成o,然后打印出来。原先的字符串s1不变。var b1 = false
var b2 = new Boolean(false)
var b = new Boolean(false)
的结果是一个为true的值。 虽然对象不只这两种声明方法,但是为了与上边的做对比我们只讲这两种。
1. var o = {}
2. var o2 = new Object()
对象的这两种声明方法没有区别。之前是基本类型和对象有区别,这里两个都是空对象,没有区别。但是o===o2的结果为false。所有新声明的对象都不相等,因为里边存的地址不同。这一点需要时刻记住。
所有的函数如果前边是window.
可以不写,比如window.alert(1)
,可以直接写成alert(1)
。
从上边两种声明方式的区别我们知道所有的对象都有toString
和valueOf
方法,如果为每个对象都单独声明这两个方法显然很浪费内存,毕竟内存条那么贵。所以我们用一个对象将这些共有属性进行封装,其他的对象直接调用这个对象的方法。但是如果说用a.toString() = xxx.toString() //xxx表示那个封装了共有属性的对象
进行手动进行绑定的话显然很麻烦,因为共有属性有很多。学过Java的同学肯定会想到继承。但是JavaScript的宗旨是简单,所以用到了一种和继承有点像的方法——原型链。由于概念这东西很不讨人喜欢,所以后边暂且不提。
在你新声明一个对象的时候js自动为你加一个属性__proto__: address
,用__proto__
来存这个保存了共用属性的对象的地址。调用的时候先在自己本身的属性里边看,如果没有的话再通过__proto__
到共有属性里边去找。
如果你想要验证的话新声明两个对象o1和o2,
o1 === o2; // false
o1.toString === o2.toString; // true.两个不同的对象调用的是同一个toString
事实上,如果你新声明一个number类型的值var n = 3
,此时再进行地址的比较n.toString === o1.toString
得到的结果是false。因为number的toString方法和object的toString方法不同。比如number的toString方法后边接参数可以转换为几进制的那个数,但是object不行。所以事实上同名方法是存在覆盖的。内部的存储方式如下图所示
新声明的number对象有__proto__
属性,它的__proto__
属性指向number的所有共有属性,而number的共有属性里又有一个__proto__
属性,指向object的所有共有属性。新声明一个对象的话,对象的__proto__
属性会直接指向object的共有属性。在调用方法的时候,js会自动通过__proto__
属性一层一层的往上找。所谓的原型链,指的就是这样的一条链。
那么这些共有属性在变量还没有声明的时候是怎么不被垃圾回收机制回收的呢?答案是全局变量window。
在一行代码都没写的时候,window就已经存在了。栈(stack)内存中的window存了window对象的地址,堆内存(heap)中window对象的属性Number(),Object(),String(),Boolean()等都被其与stack连接,这些函数的prototype属性又存储着它们的共用属性。这样的一条线避免了它们被回收。如下图所示。
在我们写了一句var o = {}
之后,生成的对象o在heap中是这样的{__proto__: address}
其中address是object的共有属性在heap中的地址,也就是函数Object的prototype属性指向的地址。即o.__proto__ === Object.prototype
。由此可推断
var n = 1;
n.__proto__.__proto__ === Object.prototype //true
声明对象的通用方法是var o = new Object()
即var 对象 = new 函数()
,所以对象.__proto__ === 函数.prototype
。由于函数有点特殊,它虽然不是七大基本类型之一但使用typeof Function
得到的却不是object而是function,而事实上我们知道函数和数组都是属于object的。所以一个函数,作为函数而言它有prototype
属性,指向函数的共有属性。作为一个对象而言,它有__proto__
属性,因为对象.__proto__ === 函数.prototype
,所以函数的__proto__
属性值和prototype
的属性值相等。即Function.__proto__ === Function.prototype
。