你不知道的JavaScript变量
主要结合最近的实际开发经验,以及著名的红宝书,在加上自己的理解,如有错误或补充,可在评论中指出,欢迎讨论。
本文主要介绍js变量中一些鲜为人知或者容易混淆的点,对常规信息采用略过形式,
由于ES5在各大浏览器中的实现标准略有差异,故而测试环境主要采用最为常用的Chrome,版本信息如下
相关信息:
- 系统:win10
- 浏览器:Chrome版本 77.0.3865.120(正式版本) (64 位)
- 非严格模式
数据类型
数据类型共分为两种,简单数据类型(也叫做基本数据类型)和复杂数据类型,前者主要包括:Number、String、Boolean以及并不常用的Undefined和Null,后者则只有Object。
堆栈问题:基本数据的保存在栈内存中,而复杂数据也就是引用类型则保存在堆内存中,在对前者复制时将会开辟一个新的内存,而对后者的复制仅仅只是复制了堆内存的指针,而对于这个存储了指针的值则是存储在了栈内存中。
这里没有对指针做过多的解释,具体定义可以查看c或者java中对指针的介绍。
不过在JavaScript中也可以认为,万物基于Object,也是本文的重点介绍对象。
至于检测相关类型,简单概括就是:基本数据类型用typeof,复杂数据类型用instanceof,这两者将会在后面频繁见到,这里不做过多介绍。
基本数据类型
Number
Number在ES中分为整数与浮点数,也可使用八进制(0)或十六进制(0x)等,如果小数点后为0,则默认解析为整数型。
由于内存限制,Number存在一个极限,如果运算结果超过了这个极限,数值就会变为无穷,即Infinity(使用isFinite()判断),在大多数浏览器中规则:
数值 | 调用方式 | |
---|---|---|
最大值 | 1.7976931348623157e+308 | Number.MAX_VALUE |
最小值 | 5e-324 | Number.MAX_VALUE |
正无穷 | Infinity | Number.NEGATIVE_INFINITY |
负无穷 | -Infinity | Number.POSITIVE_INFINITY |
此处需要介绍一个特殊的数值NaN(Not a NUmber),即非数值(没错,他叫非数值,却是一个特殊的数值),他通常用来表示,原本要返回一个数值,但是没有返回数值的情况,不会报错,常见情况有:
- 任何数值除以非数值
- 任何涉及NaN的操作
最特殊的点是,他不等于任何值,包括他自己,NaN == NaN //false
所以为了判定NaN数值,ES定义了isNaN()
isNaN(NaN); //true
isNaN(10); //false
isNaN('10'); //false
isNaN('blue'); //true,无法转化为数值
isNaN(true); //true,转换为1,
那么问题又来了,为什么字符串返回也是true了呢?
上面有写到,NaN是在原本要返回一个数值却不能返回数值的情况下返回的特殊值,在此处将字符串转换为Number过程中,无法转化,故而返回NaN。
关于将非数值转换为数值的方法:Number(),parseInt(),parseFloat(),
这里略过常见的转换规则,下面写出几个较为少见的规则:
- 在Number()下undefined返回NaN,null返回0,
- 在Number()下,字符串如果包含非数字,返回NaN,如果是纯数字则开始转换,包括整数和浮点、进制均生效,空字符串返回0,非数字字符串返回NaN
- 在parseInt()下,空字符串返回NaN,只要字符串中包含纯数字则转换,包括整数和浮点、进制均生效,遵循就近原则
- 在parseFloat()下,第二个小数点无效,只解析十进制,十六进制返回0,
Number('10abc') //NaN
parseInt('10ab20c') //10,就近原则,
parseInt('1azzz2b',16) //26
parseFloat('0xA2') //0
String
在String中,'和"没有区别,但是要前后匹配,也可以包含一些类似n,r,'的特殊字符字面量。
对于内存而言,字符串其实是不可变的,对一个字符串进行的更改实际上是进行了销毁再重建的流程。
讲一个值转换为字符串,用的最多的就是toString(),可以传参改变进制,null和undefined没有这个方法,为了解决这个问题,ES定义了转型函数String(),可以将任何类型转换为字符串。
var num=10;
num.toString(2); //'1010'
String(null); //'null'
String(undefined); //'undefined;
在ES中,很多对字符串操作的方法,同样也能对Array操作,而且用法上通常都是相同的,如concat,splice,slice等,甚至对一个字符串使用下标一样能够访问对应的字符,同时,字符串也有一些访问指定字符的方法
var str='hello world';
str[2] //'l';
str.charAt(1); //'e'
str.charCodeAt(1) //'101'返回的是字符编码,
由于字符串相关方法其实重合度挺高的,如果一一介绍过去本文会显得斑驳,所以这里不再用大篇幅去描述和贴代码,以下使用关键字+核心描述的形式介绍,
此处略过一些大家都熟用的方法如concat,splice,indexof,split等
- slice(),substring(),substr():三个方法都可以实现不改变原字符串下的字符串截取,区别在于对于参数的意义不同
- trim(),trimLeft(),trimRight():去前后空格,去前空格,去后空格
- match(),search()以及正则的exec()都可以进行字符串的模式匹配,不同的是,前两者的参数可以是正则或者匹配规则字符串,而exec则是正则去匹配字符串,其参数是需要匹配的那个字符串。
复杂数据类型
复杂数据类型只有一种,那就是object,但是由其派生的类型则有许多,例如:Function、Array等,这些都是在开发中常用的数据结构,其实,万物基于Object,
String,Number这些基本的数据类型,实际上也是一个funciton,这一点,当你在实例化一个字符串的时候就能发现
var str1=new String('dutny'); //{'dutny'}
var str2='dutny'; //'dutny'
str1==str2; //true
str1===str2; //false
如上也是可以实例化字符串的,并且在非全等条件下两种声明方式得到的字符是相等的,而在全等条件下则是表示为false,这是因为非全等条件下的比较,是经过转换后的比较,即
12=='12'; //true
12==='12' //false
实际上在上述实例化过程后,str1表现为一个object,而str2表现为一个String,由此在非全等条件下类型转换后的表现形式是相等的,即两个都是'dutny',
typeof str1; //object
typeof str2 //string
这里有点扯远了,综合上述,乍一看,似乎没有通过new操作符实例化的str2没有表现为对象,实际上并非如此,因为在js中,new操作符实例化的变量会为变量在堆内存开辟一块新的内存,而直接赋值则直接将变量保存在栈内存,故而str1表现为{'dutny'},而str2表现为'dutny'。
str1.__proto__===String.prototype; //true
str2.__proto__===String.prototype //true
这也就解释了,当你使用了直接赋值的方法,对多个变量直接赋值为同一个内容物,实际上这几个被直接赋值的变量是指向为同一块内存,因为你没有new出一块新的内存,虽然如此,但在开发中依旧不建议是用new操作符为变量赋值,一则是在实际开发中并不会出现同一个内容物多次赋值的重复性工作,二则是对内存的占用较大,即使js中有垃圾回收也难保不会出现问题。
扯了上面那么多,不过只是证明了String其实是objec的实现,其实还是那句话,在js中,万物基于Object,甚至连null也是基于Object。
typeof null //object
其实对于万物基于object这个说法,目前并不是一个官方的说法,或者说没有权威性的官方说明,只是各人对于各人的理解不同,例如说字符串的对象属性可以说成是原型设计模式而共享的来自构造函数的原型属性,可能对于我来说,这个说法或多或少还是受到了java的影响,所以比较偏向这个方向,大家不用刻意的去追求哪一个说法。
回到正题,简述了一下object,首要便是介绍Function了,由于堆栈内存的特性,所以实例化一个函数的时候,其实可以将函数名当作这个指针,也就能够理解函数的重载问题,以及函数也可以赋值。
接下来说到function的内部属性arguments和this,其实函数对于传参并没有严格的规定,你可以违反函数定义的参数数量,传递超出数量的参数,也可以少传.
function say(){
console.log('i am dutny')
};
say('tom'); //i am dutny
在这个基础上,如果我们超量了,那么怎么在函数中访问超出的那个变量呢?这就要引入arguments概念.
function say(){
console.log('i am '+arguments[0])
};
say('dutny'); //i am dutny
可以看到,arguments也是按照下标顺序访问,初次之外,arguments还有callee属性,它指向所在函数的本身,有了这个属性可以解决很多问题,例如在立即执行的匿名函数可以进行递归或其他调用自己的操作,在函数名字发生改变的情况下依旧能够正确的调用自己。除此之外,函数内部还定义了caller属性,这个属性保存着调用当前函数的函数的引用,在全局作用域中则是null,他是函数本身的属性,可以通过函数名调用,再结合上述的callee使用。
function say(){
console.log(arguments.callee.caller);
}
function person(){
let name='dutny';
say();
}
person(); //显示person的源代码,
再说this,这是一个比较复杂的点,各种详解博客也解释的非常详细,通俗点说,他指向调用当前函数的作用域。
var name='dutny_a';
function say(){
console.log(this.name);
};
var sco={name:'dutny_b'};
sco.say=say;
say(); //dutny_a
sco.say(); //dutny_b
从上可以看出:在全局作用域环境下调用say,this指向的是全局作用域,在特定的局部作用域下调用say,this指向的则是局部作用于。
更多this相关的详细信息有挺多博客详尽的描述了,这里不再赘述,有兴趣的朋友可以自己去看看博客或者相关书籍
说道this,就要引入两个由ES定义的两个非继承而来的函数的方法:apply()和call(),这两个方法都是为了去指定函数调用的作用域而出现的,不同的是,在使用call()时,需要一一指定需要传入的参数,而apply()则可以选择传入参数数组或是arguments。
var name='dutny_a';
var _this=this;
function say(age){
console.log('name:'+this.name+',age:'+age);
};
function reSay(age){
var sco={name:'dutny_b'};
say.apply(sco,arguments);
// say.call(_this,age) // name:dutny_a,age:25
}
reSay(25); //name:dutny_b,age:25
其实除了上述两个方法,ES还定义了bind()方法,也是函数的方法,只有一个参数,指定当前调用函数的作用域,这里不做赘述。
再说一说Array,
方法 | 作用 | 方法 | 作用 | |
---|---|---|---|---|
栈方法 | push() | 尾部推入 | pop() | 尾部弹出 |
队列方法 | push() | 尾部推入 | shift() | 头部弹出 |
位置方法 | indexOf() | 从头查找 | lastIndexOf() | 从尾查找 |
归并方法 | reduce() | 从头归并 | reduceRight() | 从尾归并 |
其他 | unshift() | 头部推入 |
还有常用的迭代方法,每个迭代方法接受两个参数,在每一项上运行的函数和运行该函数的作用域(可选),传入这些方法的函数接受三个参数:数组项的值、该项在数组中的位置和数组本身:
- every():如果每一项返回true,则返回true。
- filter():返回该函数会返回true的项组成的数组。
- forEach():没有返回值。
- map():返回每次函数调用的结果组成的数组。
- some():如果任一项返回true,则返回true。
除此之外,还要注意的一点是,数组的排序方法sort(),默认是按照字符串比较排序,所以会出现如下情况:
var arr=[24,1,3];
console.log(arr.sort()); //[1,24,3]
由于在字符串的比较中,'24'中的'2'是小于'3'的,所以出现了如上情况,如果想要避免这种情况,sort()可以接受一个比较函数作为参数:
var arr=[24,1,3];
function compare(value1,value2){
return value2-value1;
};
console.log(arr.sort(compare)); //[24, 3, 1]
如果想要反序可以。使用reverse()反转数组。
以上差不多就是这篇博客的内容了,本来重点在object这边,结果发现隔天再写的时候有点没有思路了,所以暂时就写这么多吧,如果有补充或者纠错的可以在评论区讨论,
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。