乐趣区

你不知道的JavaScript变量

你不知道的 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 这边,结果发现隔天再写的时候有点没有思路了,所以暂时就写这么多吧,如果有补充或者纠错的可以在评论区讨论,

退出移动版