Javascript学习笔记(一)

基本语法

语句和表达式

Javascript程序的执行单位为行,一般情况下,每一行都是一条语句,执行的顺序也是按照一行一行的执行。

语句是为了完成某个任务而进行的操作,声明一个变量

var a;

而表达式指的是为了得到返回值的计算式

3 * 7

语句和表达式的区别主要在于表达式是为了得到返回值,而语句是为了执行某项操作。

语句以分号结尾,表达式不需要分号结尾

var a;  //语句

3 * 7  //表达式

分号前面可以没有任何内容,Javascript引擎将视为空语句,但这是没有任何意义的。

;;; //三个空语句

分号在Javascript中是可选的,但是建议语句总以分号结尾,因为Javascript引擎可能猜错语句的结尾。

注释

Javascript有两种注释,一种是单行注释,一种是多行注释,单行注释以//开始后面截止这行为止都为注释部分,多行注释是以/*开始到*/结束,中间所有的内容均为注释

// 这是单行注释


/*
这是
多
行
注释
*/

由于历史上 JavaScript 可以兼容 HTML 代码的注释,所以也被视为合法的单行注释。

var a; <!-- 注释 -->

标识符

标识符是在Javascript中扮演各种语法的名称,比如变量的名称、函数的名称、属性的名称、参数等。标识符是区分大小写的,也就是说a不等于A

标识符是的定义范围可以是任何Unicode字符,但是第一个字符只能是Unicode字母、下划线_、美元符号$开头。如果是用其他字符开头,比如数字,Javascript引擎将会抛出错误

var a_1; // 定义一个以字母开头的变量

var 1_a; // 报错
//Uncaught SyntaxError: Invalid or unexpected token

ECMAScript规定了一部分保留字标识符,意思是我们不能将标识符定义为保留字,否则Javascript引擎将会抛出错误

arguments、break、case、catch、class、const、continue、debugger、default、delete、do、else、enum、eval、export、extends、false、finally、for、function、if、implements、import、in、instanceof、interface、let、new、null、package、private、protected、public、return、static、super、switch、this、throw、true、try、typeof、var、void、while、with、yield。

除了ECMAScript规定的这些保留字之外,还有三个标识符我们应该也当作保留字来对待

Infinity、undefined、NaN

变量

变量是对的引用,而变量名就是为了给这个引用起的名字,而为变量取名字的过程叫做声明变量,下面的代码中就相当于先定义了一个名为a的变量,然后把值1引用给变量,而给变量引用值的时候叫做变量赋值

var a;
a = 1;

实际上在Javascript中,在声明变量的时候,可以同时给变量赋值,下面的代码等同于上面的代码

var a = 1;

也可以同时声明多个变量,每个变量名之间用逗号分隔开

var a, b, c;

Javascript是一种动态类型的语言,变量的类型是没有限制的,也不需要提前进行声明变量是什么类型的,变量也可以随时更改类型,比如下面的变量先赋值了字符串,然后在赋值数值

var a = 'hello';
a = 1;
a // 1

一共有两种变量,分别为全局变量和局部变量,局部变量只能在定义变量的局部才能访问,全局变量是任何程序内都可以访问的变量,变量能访问的范围称为作用域,而两种创建的方式是带声明关键字var和不带声明关键字var

function a(){
  var a_value = 1;
}

a(); //执行函数a

a_value //无法访问局部变量,只能在a函数内进行范围
//Uncaught ReferenceError: a_value is not defined

下面是创建了一个全局变量

function a(){
  a_value = 1;
}

a(); //执行函数a


a_value //1

变量还存在变量提升,Javascript引擎会先解析代码,获取所有被声明的变量,然后再一行一行的运行,这个就意味着,声明变量之前访问该变量是可以访问的也不会报错,但是该变量只是被先声明了,但并没有赋值。

a
var a = 1;

实际Javascript引擎执行的顺序为

var a;
a
a = 1;

区块

Javascript使用大括号{}将多个相关的语句组合在一起,称为区块(block),对于声明变量来说,单独的区块只会生成全局变量

{
  var a = 1;
}

a // 1

条件语句

JavaScript中提供了ifswitch结构对条件进行判断,只有满足了条件过后,即条件最终的结果转换为True,才会执行相应的代码

if (布尔值) {
  语句;
}

如果if结构只有一条语句是可以省掉{}大括号,但是在语法的表达情况下,建议总使用大括号

if (布尔值) 语句;
//或者

if (布尔值)
  语句;

if结构支持一个else块,当不满足if条件的时候就会执行else块的代码

if (myvar === 0){
  myvar += 1;
} else {
  myvar -= 1;
}

除此之外if结构支持一个或者多个else...if块,当不满足if结构条件的时候会继续对下一个else...if块的条件进行判断,直到没有else...if块,则执行else块。

if (myvar === 0){
  myvar += 1;
} else if (myvar === 1) {
  myvar += 1;
} else if (myvar === 2) {
  myvar -= 1;
} else {
  myvar -= 1;
}

但是不建议使用上面这样的方式去多次判断,如果需要多次这样的判断可以使用switch,如果前面几个case代码块都没有满足条件就会执行default代码块

//用switch结构替换if结构

var myvar = 0;

switch (myvar) {
  case 0:
    myvar += 1;
    break;
  case 1:
    myvar += 1;
    break;
  case 2:
    myvar -= 1;
    break;
  default:
    myvar -= 1;
}

myvar // 1

上面的switch的每个case代码块在最后都放了一个break语句,break语句是跳出switch结构,如果没有设置break语句,会把每一个case代码块都执行一遍

//没有设置break语句
var myvar = 0;

switch (myvar) {
  case 0:
    myvar += 1; //myvar == 1
  case 1:
    myvar += 1; //myvar == 2
  case 2:
    myvar -= 1; //myvar == 1
  default:
    myvar -= 1; //myvar == 0
}

JavaScript还提供了一种简单的逻辑判断方式,三元运算符?:

(条件) ? 表达式1 : 表达式2

比如给变量赋值

var my_num = 2
var myvar = (my_num === 2) ? 10 : 20

myvar // 10

这个三元运算符可以被视为if…else…的简写形式,因此可以用于多种场合

var myVar;

console.log(
  myVar ?
  'myVar has a value' :
  'myVar does not have a value'
)
// myVar does not have a value

循环语句

JavaScript提供了几种基础的循环结构,循环主要是为了重复执行某些操作,这些循环分别有forwhiledo...while循环。

for循环有三个可选表达式,每个表达式用分号隔开,执行语句包含在大括号内,如果只有一条语句可以省略大括号,但是不建议这样做。

for (初始化表达式; 条件表达式; 执行表达式) {
  语句;
}

// 或者

for (初始化表达式; 条件表达式; 执行表达式)
  语句;

第一个为初始化表达,整个循环只会执行一次,一般初始化表达式用于初始化循环体用的变量,第二个为条件表达式,每次开始循环前都会对条件表达式进行判断,如果为true就继续执行,如果为false则停止执行循环,第三个执行表达式在每次执行完整个循环体的语句后,都会执行一次执行表达式,执行表达式一般用于对初始化的值进行自增自减这类的操作。

for (var a = 5; a !== 0; a -= 1) {
  console.log(a)  // 打印出a的值分别为 5 4 3 2 1
}

需要注意的是在for结构中的初始化表达式里面创建的变量不是只在for结构中存在,这个变量创建在和for结构一个块内。

for (var a = 5; a !== 0; a -= 1) {
  console.log(a)  // 打印出a的值分别为 5 4 3 2 1
}

a //可以对a进行访问,值为 0

while结构有一个条件,每次执行前先对条件进行判断,如果条件为true就执行,直到条件为false则停止循环,执行语句包含在大括号内,如果只有一条语句可以省略大括号,但是不建议这样做。

while (条件) {
  语句;
}

// 或者

while (条件)
  语句;

do...while结构和while结构类似,区别在于do...while结构是先执行后判断,而while结构是先判断后执行

do {
  语句;
} while (条件);

// 或者

do
  语句;
while (条件);

循环控制

JavaScript提供了两种用于控制循环执行的语句,分别为continuebreak语句买,这两种语句都可以控制循环结构的执行顺序。

continue语句的作用是终止本轮循环,然后继续执行下一次循环

var a = 5;

while (a !== 0) {
  a -= 1;
  if(a === 2){
    continue;
  }
  console.log(a);
}

// 输出的值分别为:4 3 1 0

上面的代码首先声明了一个变量并赋值为5,然后用于循环判断a不等于0,如果当a等于0就停止循环,循环体内的代码首先每次执行会对变量a进行减1,然后有一个if条件结构,判断a是否等于2,如果等于2就终止当前循环,继续执行下一个循环。

因为上面的循环体内第一段代码是对变量a减1,所以第一次循环的时候会减去1,输出就是从4开始,循环体内的代码判断a等于2就跳过,所以输出的结果就不会包含2。

break语句用于终止跳出循环,即跳出包裹它的循环,如果它被被两层循环包裹,就跳出离它最近的循环

var a = 5;

while (a !== 0) {
  a -= 1;
  if(a === 2){
    break;
  }
  console.log(a);
}

// 输出的值分别为:4 3

标签

Javascript允许语句前面有标签label,标签相当于定位符,可以配合语句进行跳转,跳转的不止是循环,可以是代码块等。

label:
  语句;

比如通过标签对上面的while进行控制,当a等于3的时候就不在执行循环了

var a = 5;

top:
while (a !== 0) {
  a -= 1;
  if(a === 3){
    break top;
  }
  console.log(a);
}


console.log('循环结束');
// 输出的值分别为:4和循环结束

标签也可以用于跳出代码块

top: {
  console.log(1);
  break top;
  console.log('不会输出');
}

console.log(2);

//输出 1
//输出 2

continue语句也可以与标签配合使用

top:
  for (var i = 0; i < 3; i++){
    for (var j = 0; j < 3; j++){
      if (i === 1 && j === 1) continue top;
      console.log('i=' + i + ', j=' + j);
    }
  }
// i=0, j=0
// i=0, j=1
// i=0, j=2
// i=1, j=0
// i=2, j=0
// i=2, j=1
// i=2, j=2

上面代码中,continue命令后面有一个标签名,满足条件时,会跳过当前循环,直接进入下一轮外层循环。如果continue语句后面不使用标签,则只能进入下一轮的内层循环。

参考链接

《JavaScript 教程》 – 阮一峰

《Speaking JavaScript》 – Dr. Axel Rauschmayer

数据类型

导论

Javascript中数据类型分为两种类型的值,分别为原始值和对象,其中原始值可以称为不可变值,原始值分别有:

  1. 数值number:如整数3和分数3.1
  2. 字符串string:由0或者多个字符组合,比如javascript
  3. 布尔值boolean:表示真和假的两个值,这个类型只有两种值,即truefalse,表示真假
  4. undefined:表示未定义和没有价值的意思,当定义了一个变量但是为赋值的时候,这个变量的值就等于undefined表示没有价值
  5. null:表示一个空对象,或者空值。

上面五种类型中的undefinednull区分有一点难度,但是通过函数来区分这两个值,下面print_var函数,需要两个参数分别为ab,调用这个参数的只传入null表示这是一个空值,然后输出的结果中可以看见,只传入了一个参数,而另外一个参数默认为undefined表示未定义。

function print_var(a, b){
  console.log(a);
  console.log(b);
}


print_var(null);

// null
// undefined
// undefined 这里的一个undefined是如果函数没有返回值就会自动返回一个

对象类型又被称为集合类型或者引用类型,因为对象类型可以包含所有类型的值,原始值或者对象类型都可以包含,这里的对象类型实际上是一种统称,在Javascript中对象类型分为三种,分别为:

  1. 对象object
  2. 数组array
  3. 函数function

《JavaScript语言精辟》这本书里面提到这三种类型的定义:

数组是对象,函数是对象,正则表达式也是对象,当然,对象也是对象。

所以可以将对象类型看作一种定义,而上面这些对象都是属于对象类型。

字符串

字符串是由0或者多个字符连以起的字符,比如'javascript',字符串放在单引号和双引号之中

'javascript' //单引号字符串

"javascript" //双引号字符串

同时在单引号中用双引号和双引号中用单引号都是没有问题的,如果必须要在单引号中用单引号或者双引号中用双引号,可以通过在引号前面用\符号进行转义

'java"scr\'ipt' // java"scr'ipt

由于HTML属性值使用的是双引号,再加上很多项目都规定使用单引号,所以建议使用单引号,当然使用双引号也可以,但请不要在代码中使用单引号的同时又在使用双引号。

字符串默认只允许定义在一行内,如果定义在多行内将会报错

'a
b
c'  //Uncaught SyntaxError: Invalid or unexpected token

如果需要多行定义字符串,可以使用两个办法,一个是\反斜杠,如果是用反斜杠\需要注意的是在反斜杠后只能包含换行符,不能有任何的字符,包括空格都不行。

'a\
b\
c'
// abc

另一个是+加号运算符

'a'
+ 'b'
+ 'c'
// abc

创建字符串

创建字符串有两种方式,分别为字面量和构造函数

  • 'hello':字面量
  • new String('hello'):构造函数

常用的都是通过字面量来进行定义,但是这里需要注意的是通过构造函数返回的字符串是一个对象,即一种字符串对象,这样的字符串对象可以当字符串原始值使用,但是它不是一个原始值,而是一个对象值。

var a = 'hello';

typeof a // string

var b = new String('hello');

typeof b // object

访问字符串

字符串的访问可以通过[]方括号访问,这是一种类数组的访问方式,如果当访问的数量大于所有的字符长度,将会返回undefined

var a = 'javascript';

a[0] // j
a[1] // a

a[-1] //不支持倒数访问

从上面的情况来看,如果不知道一个字符串的长度该怎么办?Javascript提供了一个字符串属性length判断字符串的长度,而这个属性的值减去1,就得到用方括号访问字符串的最大数

var a = 'javascript';


a.length // 10

a[9] //t

a[10] //undefined

字符串的不可变

实际上一旦创建了字符串后,字符串是不能变的。

var a = 'javascript';

a[0] = 'b';

a // javascript

a[1] = 'c';

a // javascript

delete a[0];

a // javascript

但是可以通过重新对变量进行赋值,更改字符串,但是此时的更改字符串,是属于先把变量内的字符串丢弃后,重新赋值新的内容,所以更改的不是字符串,而是重新给变量赋值了新的内容

var a = 'javascript';

a[0] = 'b';

a // javascript

a = 'bava';

a // bava

字符串的编码

在字符串的内部还有一部分义的转移字符,除了普通的可打印字符以外,一些特殊有特殊功能的字符可以通过转义字符的形式放入字符串,转义字符需要用\进行转义,比如\n代表换行符,如果把下列的这些字符通过转义后都会变成特殊的字符,下面的列表中列出来部分转义字符

  • \0 :null(\u0000)
  • \b :后退键(\u0008)
  • \f :换页符(\u000C)
  • \n :换行符(\u000A)
  • \r :回车键(\u000D)
  • \t :制表符(\u0009)
  • \v :垂直制表符(\u000B)
  • \’ :单引号(\u0027)
  • \” :双引号(\u0022)
  • \ :反斜杠(\u005C)

这些转义字符都是有对应的Unicode编码码点,所以实际上我们的转义字符都是对Unicode进行转义,所以对Unicode编码进行显示的方式有三种。

(1)\HHH

反斜杠后面紧跟三个八进制数(000到377),代表一个字符。HHH对应该字符的 Unicode 码点,比如\251表示版权符号。显然,这种方法只能输出256种字符。

(2)\xHH

\x后面紧跟两个十六进制数(00到FF),代表一个字符。HH对应该字符的 Unicode 码点,比如\xA9表示版权符号。这种方法也只能输出256种字符。

(3)\uXXXX

\u后面紧跟四个十六进制数(0000到FFFF),代表一个字符。XXXX对应该字符的 Unicode 码点,比如\u00A9表示版权符号、\u2665是一个实心的爱心

JavaScript 使用 Unicode 字符集,JavaScript 引擎内部,所有字符都用 Unicode 表示,Javascript以Unicode进行存储,那么Javascript引擎也允许使用Unicode码点表示字符,比如上面的转义字符,就是使用的Unicode码点进行表示的。

程序中可以使用Unicode表示字符,Javascript引擎会自动识别是一个字符是字面形式还是Unicode形式,但是一旦由Javascript引擎输出给用户的时候都是以字面形式表示。

var \u0061 = 'javascript';

a // 'javascript'  0061码点对应的字符就是a

\u0061 // 'javascript' 同样可以以码点进行访问

每个字符在Javascript内部都是以16位(即2byte)存储,也就是说Javascript的字符长度固定在16位,但是UTF16编码有两种长度,对于码点在U+0000U+FFFF的范围内的字符,长度即在16位内,但是UTF16还有一种长度在32位码点(即4byte),即U+10000U+10FFFF,并且规定前两个字节在0xD8000xDBFF之间,后两个字节在0xDC000xDFFF之间。

JavaScript 对 UTF-16 的支持是不完整的,由于历史原因,只支持两字节的字符,不支持四字节的字符。这是因为 JavaScript 第一版发布的时候,Unicode 的码点只编到U+FFFF,因此两字节足够表示了。后来,Unicode 纳入的字符越来越多,出现了四字节的编码。

但是,JavaScript 的标准此时已经定型了,统一将字符长度限制在两字节,导致无法识别四字节的字符。上一节的那个四字节字符𝌆,浏览器会正确识别这是一个字符,但是 JavaScript 无法识别,会认为这是两个字符,所以在32位utf-16编码范围的码点,Javascript总是将其识别为两个长度

'𝌆'.length // 2

Base64编码

文本中有时会包含一些不可打印的字符,如ASCII编码中的0到31,这些字符都是控制字符或通信专用字符,这是制定组织将其分配的并不可打印的,那么怎么将其进行打印呢?

Javascript原生提供了一种Base64的编码解码,Base64编码是可以将任意值转成为0~9A~Za~z+-这个64个字符组成的可打印字符,这个编码的主要目的不是为了加密,而是为了不让特殊字符出现。

Javascript提供了两种方法对其进行编码和解码,分别为btoa用于编码和atob用于解码

btoa('javascript') //"amF2YXNjcmlwdA=="

atob('amF2YXNjcmlwdA==') //"javascript"

这两个方法只支持ASCII字符,如果用其他字符将会报错

btoa('你')  //报错

URI编码

有一种方法可以将其他编码转换为ASCII编码范围内的码点,就是URI编码,URI编码规定url地址中,只能包含0-9的数字、大小写字母a-zA-Z、线-_.~以及! * ' ( ) ; : @ & = + $ , / ? # [ ],但是我们的url中不止这些字符,这时候就需要我们进行编码传输,需要将这些特殊字符换成相应的十六进制的值。

Url编码通常也被称为百分号编码(Url Encoding,also known as percent-encoding),是因为它的编码方式非常简单,使用%百分号加上两位的字符——0123456789ABCDEF——代表一个字节的十六进制形式。Url编码默认使用的字符集是US-ASCII。例如a在US-ASCII码中对应的字节是0x61,那么Url编码之后得到的就是%61,我们在地址栏上输入http://g.cn/search?q=%61%62%63,实际上就等同于在google上搜索abc了。又如@符号在ASCII字符集中对应的字节为0x40,经过Url编码之后得到的是%40。

对于非ASCII字符,需要使用ASCII字符集的超集进行编码得到相应的字节,然后对每个字节执行百分号编码。对于Unicode字符,RFC文档建议使用utf-8对其进行编码得到相应的字节,然后对每个字节执行百分号编码。如”中文”使用UTF-8字符集得到的字节为0xE4 0xB8 0xAD 0xE6 0x96 0x87,经过Url编码之后得到”%E4%B8%AD%E6%96%87″。

Javascript中为URI编码提供了两种方法encodeURIComponent用于编码和decodeURIComponent用于解码,而这连个方法使用的URI编码可以将其utf-8的字符编码为ASCII编码支持的范围内,我们可以结合上面的btoaatob两个方法。

encodeURIComponent('你好') //"%E4%BD%A0%E5%A5%BD"
decodeURIComponent('%E4%BD%A0%E5%A5%BD') //你好


btoa(encodeURIComponent('你好'))  //"JUU0JUJEJUEwJUU1JUE1JUJE"
//将中文字符的utf8编码转换为ASCII支持的字符,然后在转换为Base64编码


decodeURIComponent(atob('JUU0JUJEJUEwJUU1JUE1JUJE'))  //你好
//将中文字符的Base64编码通过转换成原始字符

数值

Javascript内部所有的数值都是以双精确度64位浮点数进行储存的,这也就是说Javascript的底层是没有整数的,所以整数和浮点数在Javascript中是相等

1 === 1.0 // true
3 === 3.0 // true

而如果某些运算只能通过整数才能完成的,Javascript引擎会自动将64位的浮点数转换为32位整数,然后在进行运算。

创建数值

创建数值有两种方式,分别为字面量和构造函数

  • 123:字面量
  • new Number(123):构造函数

常用的都是通过字面量来进行定义,但是这里需要注意的是通过构造函数返回的数值是一个对象,即一种数值对象,这样的数值对象可以当数值原始值使用,但是它不是一个原始值,而是一个对象值。

var a = 123;

typeof a // number

var b = new Number(123);

typeof b // object

数值精确度

现在计算机中编程语言采用浮点数的标准几乎都是采用的IEEE754

而64位双精度浮点数该标准规定的是:

  • 就是1位最高位,即第63位,表示符号位,0表示正,1表示负

  • 52-62位表示指数部分,即科学表示法中的指数部分,大小范围就是0到2047

  • 51-0表示尾数部分,即实际有效数字部分

比如54846.3这个数值,用科学记数法为5.48463e4,如果按照标准划分,就该为63位为0表示正数,52-62位为4表示指数部分,0-51则为5.48463表示尾数部门。

符号位决定了一个数的正负,指数部分决定了数值的大小,小数部分决定了数值的精度。

由于第63位表示的是符号数,而符号位对于每一个数值都是一样看待,所以就会出现-0+0,但是这两个值在Javascript中是相等的,但是因为符号位不同,所有会显示-0+0

指数部分一共有11个二进制位,因此大小范围就是0到2047。IEEE 754 规定,如果指数部分的值在0到2047之间(不含两个端点),那么有效数字的第一位默认总是1,不保存在64位浮点数之中。也就是说,有效数字这时总是1.xx...xx的形式,其中xx..xx的部分保存在64位浮点数之中,最长可能为52位。因此,JavaScript 提供的有效数字最长为53个二进制位,公式为下面这样

(-1)^符号位 * 1.xx...xx * 2^指数部分

精度最多只能到53个二进制位,这意味着,绝对值小于2的53次方的整数,即-2^{53}2^{53},都可以精确表示,所以Javascript对于15位内的十进制数都可以精确处理,如果超出这个范围就会存在精准度的问题了。

Math.pow(2, 53) // 9007199254740992

Math.pow(2, 53)+1  // 9007199254740992

Math.pow(2, 53)+2 // 9007199254740994

Math.pow(2, 53)+3 // 9007199254740996

由于这个标准将十进制转换为二进制的时候会造成丢失精度,所以涉及到小数的运算比较等情况的时候要注意这些问题

0.1 + 0.2 === 0.3 //false

0.3 / 0.1 // 2.9999999999999996

(0.3 - 0.2) === (0.2 - 0.1) // false

0.100000000000000002 === 0.1000000000000000030 // true

数值的范围

IEEE 754 规定,64位浮点数的指数长度是11个二进制位,即指数最大为2047(因为从0开始所以2048-1),而其中需要分出一半表示负数,则能表示的数值范围在2^{1024}2^{-1023}

如果超出了2^{1024}这个范围,则会发生正向益处,即Javascript无法表示这么大的数,则会返回Infinity

如果超出了2^{-1023}这个范围,则会发生负向益处,即Javascript无法表示这么小的数,则会返回0

如果一个数大于等于2的1024次方,那么就会发生“正向溢出”,即 JavaScript 无法表示这么大的数,这时就会返回Infinity

JavaScript提供Number对象的MAX_VALUE和MIN_VALUE属性,返回可以表示的具体的最大值和最小值。

Number.MAX_VALUE // 1.7976931348623157e+308
Number.MIN_VALUE // 5e-324

数值的表示方法

Javascript中的数值表示方法有很多种,比如常用的十进制和其他进制以及科学记数法

13 //十进制字面形式
0xFF // 255
2e1 // 20

Javascript提供了四种进制的表达方法,都可以用来表示数值分别为:

  • 十进制:没有前导0的数值。
  • 八进制:有前缀0o或0O的数值,或者有前导0、且只用到0-7的八个阿拉伯数字的数值。
  • 十六进制:有前缀0x或0X的数值。
  • 二进制:有前缀0b或0B的数值。

如果Javascript的小数点前多于21位、小数点后的零多余5个会自动转为科学记数法

1234567890123456789012
//1.2345678901234568e+21

0.0000003 
// 3e-7

特殊值

JavaScript有几个特殊的数字值:

  • 两个错误值:NaNInfinity,这个两个特殊值都是number类型。
  • 两个零:+0-0,这个原因在前面的章节数值精确度那里已经说明了。

NaN(Not a Number)是一个特殊的错误值,属于number类型,NaN表示不是一个数值,一般由以下几种操作的时候会返回NaN

  • 无法解析为数字:如Number('javascript')
  • 操作失败:如Math.acos(2)
  • 其中一个数为NaN的运算:如NaN + 1

NaN与自身是不相等的,即使是严格相等也是不相等的

NaN === NaN //false

如果要对NaN进行判断,可以使用Javascript提供的一个全局函数isNaN,该方法会对非数值的值进行转换,再进行判断,但是建议在使用isNaN方法前先判断是否为数值类型,以免引起未知的问题发生

isNaN(NaN) //true

function myIsNan(value){
    return typeof value === 'number' && isNaN(value);
}
//上面这个函数对一个数值进行检查是否为NaN,同时也会检查该值是否为数值。

Infinity错误值表示正无穷,如果超出上面章节所说的$2^1024$至$2^-1023$这个范围,就会返回Infinity,既然IEEE 754标准规定的64位双精准浮点数有一位是表示正负之分的,那么Infinity也有正负之分,Infinity表示正无穷,而-Infinity表示负无穷

Infinity大于一切数值(除了NaN),-Infinity小于一切数值(除了NaN)。

Infinity > 1000 // true
-Infinity < -1000 // true

InfinityNaN比较,总是返回false

Infinity > NaN // false
-Infinity > NaN // false

Infinity < NaN // false
-Infinity < NaN // false

Infinity的四则运算,符合无穷的数学计算规则。

5 * Infinity // Infinity
5 - Infinity // -Infinity
Infinity / 5 // Infinity
5 / Infinity // 0

0乘以Infinity,返回NaN;0除以Infinity,返回0Infinity除以0,返回Infinity

0 * Infinity // NaN
0 / Infinity // 0
Infinity / 0 // Infinity

Infinity加上或乘以Infinity,返回的还是Infinity

Infinity + Infinity // Infinity
Infinity * Infinity // Infinity

Infinity减去或除以Infinity,得到NaN

Infinity - Infinity // NaN
Infinity / Infinity // NaN

Infinitynull计算时,null会转成0,等同于与0的计算。

null * Infinity // NaN
null / Infinity // 0
Infinity / null // Infinity

Infinityundefined计算,返回的都是NaN

undefined + Infinity // NaN
undefined - Infinity // NaN
undefined * Infinity // NaN
undefined / Infinity // NaN
Infinity / undefined // NaN

正负无穷都可以用相等和严格相等进行判断

Infinity === Infinity //true

-Infinity === -Infinity //true

全局函数isFinite

此外,全局函数isFinite允许检查值是否是实际数字(既不是Infinity的也不是NaN):

isFinite(5)
//true 
isFinite(Infinity)
//false 
isFinite(NaN)
//false

布尔值(Boolean)

表示真和假的两个值,这个类型只有两种值,即truefalse,表示真假,下列的运算符会返回布尔值:

  • 前置逻辑运算符:!(NOT)
  • 相等运算符:=、!、!=
  • 比较运算符:>、>=、<、<=

Javascript某个运算的预期位置的值应该是布尔值,那么Javascript会自动转为布尔值,下列中的值都会转换为false,其他的任何值会被转换为true

  • undefined
  • null
  • false
  • 0
  • NaN
  • “”或”(空字符串)

包括空数组[]和空对象{}都会被转换为true,因为按照对象结构来说,即使是内部没有值,但这个对象确实存在的,所以它们被转换为true

创建布尔值

创建布尔值有两种方式,分别为字面量和构造函数

  • false:字面量
  • new Boolean(false):构造函数

常用的都是通过字面量来进行定义,但是这里需要注意的是通过构造函数返回的布尔值是一个对象,即一种布尔值对象,这样的布尔值对象可以当布尔值原始值使用,但是它不是一个原始值,而是一个对象值。

var a = false;

typeof a // boolean

var b = new Boolean(false);

typeof b // object

这里需要特别注意的就是当使用构造函数建立布尔值的时候然后进行if语句判断,不管是真还是假,if语句都会执行,因为根据Javascript转换规定,对象返回的都是真,具体转换规则可以查看类型转换一章,所以创建值都使用字面量可以避开很多问题

var b = new Boolean(false);

if (b){
    console.log(1) //   1 执行成功
}

undefined 和 null

1995年Javascript诞生的时候,只设计了null表示无,根据C语言的传统,null可以转换为0,因为Javascript也是参照了C语言,所以在javascript中,null也是可以转换为0

Number(null)  //0

2 + null // 2

JavaScript的设计者Brendan Eich觉得表示无的值最好不要是一个对象,其次,那时的 JavaScript 不包括错误处理机制,Brendan Eich 觉得,如果null自动转为0,很不容易发现错误。

因为Brendan Eich又设计了一个undefined,两个值的区别就在于null是一个表示空的对象,可以转换为数值0

undefined表示未定义和没有价值的意思,当定义了一个变量但是为赋值的时候,这个变量的值就等于undefined表示没有价值,undefined转换为数值为NaN

Number(undefined) //NaN

1 + undefined //NaN

上面五种类型中的undefinednull区分有一点难度,但是通过函数来区分这两个值,下面print_var函数,需要两个参数分别为ab,如果我们对其中一个参数传入null表示这个参数是一个空值,而如果没有传入参数,则在函数内部,这个参数为undefined表示未定义。

function print_var(a, b){
  console.log(a);
  console.log(b);
}


print_var(null);

// null  //参数a参入了null值,表示是一个空值而不是未定义
// undefined 参数b是未定义参数,所以为undefined
// undefined 这里的一个undefined是如果函数没有返回值就会自动返回一个

对象

Javascript的基本类型有数值Number字符串String布尔值Booleannullundefined五种类型,其他的值都是对象,就如《JavaScript语言精辟》这本书里面提到的定义:

数组是对象,函数是对象,正则表达式也是对象,当然,对象也是对象。

Javascript中的对象是属于无类型的,意思是无限制的,如对象里面放对象,对象里面放数组、函数等,除此之外,Javascript对象还包含一种原型链的特性,允许对象继承另一个对象的属性。

创建一个对象很简单,有两种方式可以创建一个对象,分别为

  • 对象表达式:通过{}大括号直接可以创建一个空对象

  • 构造函数:Object构造函数为给定值创建一个对象包装器。如果给定值是 nullundefined以及空,将会创建并返回一个空对象,否则,将返回一个与给定值对应类型的对象。

var a = {};  //通过对象字面量

var a = new Object();  //通过构造函数

键名

Javascript对象是一种键值对的无序数据结构,大概如下面这样:

var a = {}; //初始化一个变量a并赋值一个空对象

var a = {
  language : 'javascript'   //初始化一个变量a,并赋值一个对象,对象里面含一个键名为language的属性
};

对象的每个键值对之间用逗号,分隔,每个属性与值之间用分号:分隔,最后一个键值对可以加逗号也可以不加逗号,对象的所有键名都是字符串,所以即使不用加引号表示这是一个字符串Javascript引擎会自动转换为字符串的

var a = {
  'language': 1,
  language: 2
};

a // {language: 2} 实际上这里创建的带引号和不带引号的都是一样的字符,所以由最后一个language的键名定义覆盖了前面定义的。

所有的字符都会被转换为字符串,包括数值

var a = {
  1: 1,
  2: 2
};

a // {1:1, 2:2} 实际上健名都是字符串

对象所有的键名所有都是字符串,但是如果没有用引号就必须符合标识符的规则,如前面第一个字符不能是数值,但是如果用引号包裹键名,那么键名就可以是任意的字符串,所以请尽可能的符合标识符的规则。

var a = {
  1a: 222   //报错
};


var a = {
  '1a': 222 //正常执行
};

Javascript中的对象是属于无类型的,意思是无限制的,如对象里面放对象,对象里面放数组、函数等

var a = {
  '1a': {}
};

//定义了一个对象a,里面一个名为'1a'的键的值又为一个空对象

对象里面将键值对分为两类:

  • 属性:值可以为任何的数据类型(除函数),
  • 方法:值为一个函数
var a = {
  'language': 'javascript',
  'get': function(){ return 'get' }
};

// 其中的language为属性,get为方法。

读取对象

需要访问、读取变量有两个方法,一种是通过方括号[]运算符,使用方括号[]运算符需要将字符串用引号包裹,如果不包裹的话,Javascript引擎会将该字符认为是变量,然后再用变量的值来访问对象。

如果是数值,方括号[]运算符将会自动转换为字符串,实际上给数值将引号和不加引号是相等的。

var a = {
  'language': 'javascript',
  '2': 2
};

a['language'] // javascript
a[2] // 2
a['2'] // 2

a[2] === a['2'] //true

var b = 'language';

a[b] //javascript

除此之外,方括号[]运算符还可以使用表达式

var a = {
  'language': 'javascript'
};

a['lang' + 'uage'] //javascript

还有一种使用点.运算符,这样方法是必须符合标识符规则的,比如不能以数字开头,因为会被当前小数点,如果要访问数字键名,请用方括号[]运算符所以请尽可能的符合标识符的规则。

var a = {
  'language': 'javascript',
  '2': 2
};

a.language //javascript

a.2  //报错

a[2] //2
a['2'] //2

属性赋值

如果要对一个属性进行赋值,如果都在避免了标识符的规则下,有三个方式,分别为初始化赋值、方括号[]运算符赋值、点.运算符赋值。

var a = {
  'num': 1   //初始化赋值
};

//点运算符赋值
a.num //1
a.num = 2;
a.num // 2

a['num'] // 2 由上面的点运算符修改后
a['num'] = 3;
a['num'] // 3

不管是哪种方式赋值,三种方式效果都是一样的

var a = {
  'num': 2
};

// 等价于

var a = {};
a.num = 2;

// 等价于

var a = {};
a['num'] = 2;

对象的引用

一个变量的值是对象的话可以称为这个变量引用了这个对象,那么多个变量都引用这个对象,那么它们都是相等的

var a = {};

var b = a;

a === b // true

既然是引用的同一个对象,那么当其中一个引用该对象的变量更改了这个对象,也会影响到所有引用该对象的变量

var a = {};

var b = a;

b // {}

a.num = 1;
b // {num: 1} 

变量中存储的原始值和对象本质是一样的,都是值,但是如果存储的是原始值实际上这个变量内部就是原始值,如数值1,布尔值true等。

但如果变量中存储的是对象,可以想象为是存储的值为一个内存地址,这个内容地址指向的是一个对象,所以如果多个变量之间都是这个内存地址,那么多个变量都是指向的同一个对象,如果对其中的一个变量的值进行重新赋值,那么实际上你修改的是变量中存储的内存地址而并不影响这个对象,但如果你通过方括号[]运算符赋值、点.运算符赋值,实际上是对这个变量值中内存地址指向的对象进行赋值,所以影响的是所有指向这个对象的变量

所以从上面的结论中来看,你可以将任何引用类型当做值来传递,因为它们始终传递的是一个内存地址,而内存地址在计算机中也是一串原始值。

区块还是表达式

在基本语法中提到Javascript使用大括号{}将多个相关的语句组合在一起,称为区块(block),那么对象也是通过大括号{}定义的

{language: 'javascript'}

Javascript引擎读到上面这行代码的时候,会有两种可能的含义:

  • 这是一个表达式,表示一个对象
  • 这是一个语句,表示标签language指向字符串 javascript

为了避免这样的歧义,V8(一种Javascript引擎)引擎规定,如果首行为大括号{},则解释为对象,但是为了避免歧义,建议在大括号前面加上圆括号,这样就表示这个表达式返回一个对象

({language: 'javascript'}) //{language: 'javascript'}

这种问题在eval语句中反映最明显,eval语句是用于执行Javascript代码的语句

eval('{language: "javascript"}')  
//返回字符串javascript

eval('({language: "javascript"})')
//返回一个对象 {language: "javascript"}

操作对象

如果要查看对象的所有键名,可以使用Object.keys方法,该方法返回一个数组,将包含传入的对象中所有的键名

var a = {language: 'javascript'};

Object.keys(a); // ['language']

删除一个属性可以使用delete操作符,如果删除成功则返回true,否则返回false,删除成功过后如果再返回删除的属性,将会得到undefined

var a = {language: 'javascript'};

delete a.language // true

a.language // undefined

delete操作符对于不存在的属性执行删除操作,也会返回true,所以不能够依照delete操作符来判断属性是否存在

var a = {};

delete a.language // true

delete操作符需要注意的是有两种情况无法删除属性,第一种是因为该属性存在并不能删除,第二种是无法删除除了本身的属性之外的继承属性。

查询和遍历

查询和遍历都会涉及到一个问题,一个对象创建的时候,会继承一些属性和方法,那么去查询、遍历这个对象含有这个属性或者方法的时候也会把这些属性和方法都包含在内。

对象有一个in操作符可以来检查某个对象的键名是否存在

var a = {language: 'javascript'};

'language' in a // true

'javascript' in a // false

'toString' in a //true 继承方法也会返回true

for...in循环用来遍历一个对象的全部属性,for...in有两个特点:

  • 它遍历的是对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性。
  • 它不仅遍历对象自身的属性,还遍历继承的属性。

比如,虽然toString方法是继承的,但是toString可遍历属性为不可遍历,所以下面的遍历不会输出toString

var a = {language: 'javascript'};

for (var i in a){
    console.log(i, a[i]) //输出键名和值
}

//language javascript

数组

数组是按次序排列的一组值,用方括号包裹所有值,每个值都有一个编号,编号从0开始,JavaScript 使用一个32位整数,保存数组的元素个数。这意味着,数组成员最多只有 4294967295 个(232 – 1)个。

var arr = [1, 2, 3, 4, 5];

上面的代码用数组表达式创建了一个数组,其中的值为1-5,编号从0-4

创建数组

创建数组的方式有两种:

  • 通过数组表达式:通过[]方括号直接可以创建一个空数组
  • 通过构造函数:通过Array构造函数创建一个数组,Array构造函数接受多个参数,这些参数会将作为新数组的每一个值。如果没有参数则返回一个空数组,即一个值都没有。如果只有一个参数,这个参数的值代表创建有多少个值的数组,每个值都为空。
var a = []; //通过数组表达式创建了一个空数组

var a = new Array(); //通过构造函数创建了一个空数组

var a = new Array(5);  //[empty * 5]
//通过构造函数创建了有5个空值的数组

var a = new Array(1, 2, 3);
//通过构造函数创建了有三个值的数组,值分别为1, 2, 3

数组Array是一个特殊的对象,typeof操作符返回的也是object类型,因为数组的编号是从0访问的,这个编号也可以认为是属性,所以如果要访问数组的值只能通过方括号[]访问。

数组是一个特殊的对象,通过方括号进行访问,如果是能转换为数值并且在正确的表示范围内就是访问的数组的数据结构数据,如果不能够转换,比如超过了数值的最大表示范围就会自动转换为字符串而访问属性、方法

var arr = [1, 2, 3, 4, 5];

arr[0] //1
arr['0'] //1  

arr[0] === arr['0'] //true 字符串0和数值0表示的都是同一个值

arr[Math.pow(2, 32)] = 'b';  //设置超出数值的表示范围

arr[4294967296] // "b"  //会将超出数值范围的值转换为字符串


并且数组也同样有属性、方法,因为数组本身也是对象

var arr = [1, 2, 3, 4, 5];

arr.language = 'javascript';

arr.language //javascript

arr['language'] //javascript

既然数组也是对象,那么Object.keys方法也可以来查看数组有哪些属性,这里返回的是数组的索引加属性

var arr = [1, 2, 3, 4, 5];

arr.language = 'javascript';

Object.keys(arr);
//["0", "1", "2", "3", "4", "language"]

数组中的length属性可以查询数组中含有多少个值

var arr = [1, 2, 3, 4, 5];

arr.length // 5

length属性是动态可写的,意思是这个值是可以改变的,如果设置这个值大于当前的成员数量,那么将设置的值减去成员数量后的个数,然后填充并将新添加的每个成员的值都是为空位。

var arr = [1, 2, 3, 4, 5];

arr.length = 7;

arr // [1, 2, 3, 4, 5, , ]  //后面添加了两个空值

length属性如果设置为比当前成员数量还小的数值,那么,将会从最后面的成员开始减少到和length设置的值一致

var arr = [1, 2, 3, 4, 5];

arr.length = 4;

arr // [1, 2, 3, 4] 

通过设置length属性,可以直接快速清空数组

var arr = [1, 2, 3, 4, 5];

arr.length = 0;

arr // [] 

设置数组的值和对象一直的,可以运用点操作符,但是数组的成员索引值都是数字,所有都只能通过方括号访问,而其他符合标识符规定的可以用点操作符

var arr = [1, 2, 3];

arr[0] = 'hello';  //修改数组成员

arr //['hello', 2, 3]

arr.language = 'javascript';

arr 
//0: 'hello'
//1: 2
//2: 3
//language: "javascript"

查询遍历

数组同样支持for...in,但是这个遍历还是基于键名,如果是数组相当于返回的索引值,比如下面的数组有五个成员,那么就有五个键,为0-4,并且for...in还会返回可遍历的属性或者方法

var arr = [1, 2, 3, 4, 5];

arr['language'] = 'javascript';

for (var i in arr){
    console.log(i)
}

// 0 1 2 3 4 language

in也可以用于数组,但是查询的也是基于键名的,比如下面有五个成员,那么0-4键名都是存在的

var arr = [1, 2, 3, 4, 5];

1 in arr //true

5 in arr //false

函数

函数的作用是将一些常用的代码块进行封装,如果需要用到这些代码块的时候就可以直接调用这个函数,而不用重复再写这些代码。

JavaScript中的函数实际也是对象,和数组一样都是一种特殊的对象,只是函数内部存储的是一块代码,同样有属性和方法。

function language() {
    return 'javascript';
}

language['num'] = 1; //设置属性

language['num'] // 1 使用方括号操作符
language.num // 1 使用点操作符

声明函数

声明函数有三种方法,分别为:

  • function命令创建
  • 函数表达式创建
  • 构造函数创建

function命令创建函数是通过function命令,然后后面跟着函数名以及圆括号()然后是大括号{},圆括号是定义参数,每个参数用逗号分隔,而大括号就是定义函数的执行代码,称为函数体。

下面的函数创建了一个名为language的函数,并要求传入一个编程语言的名称,并返回这个值。

function language(name) {
    return name;
};

language('javascript') //javascript  调用函数并返回传入参数的值

函数表示式也是通过function命令创建的,然后将这个函数返回给一个变量,但是可以不用创建函数名

var language = function(name) {
    return name;
};

language('javascript') //javascript

函数表达式也可以创建带有函数名的函数,但是加上了函数名过后,该函数名只有在函数体内部有效,而在外部是无效的

var language = function a(name) {
    return name;
};

a('javascript') //报错:ReferenceError: a is not defined

构造函数创建的方式是通过new命令加Function构造函数进行创建的,构造函数接收三个参数,第一和第二个参数都为定义的函数需要传入的参数,然后第三个参数为函数体的代码,如果只传入一个参数,这个参数即为函数体代码

var language = new Function(
    'x',
    'y',
    'return x + y;'
);

language('javascript', ' hello') // javascript hello

而重复声明函数将会以最后一个声明的为准

function a() {
  console.log(1);
}

function a() {
  console.log(2);
}

a() // 2

函数的返回值以及函数提升

每个函数都有返回值,函数内部用return主动返回一个返回值,而当我们没有定义这个return语句的时候,Javascript引擎会自动返回undefined,比如下面的代码,实际是相等的

function a() {
  console.log(1);
}

// 相等

function a() {
  console.log(1);
  return undefined;
}

当Javascript引擎遇见了return语句就立即返回,就不再执行函数体后面的语句了

function a() {
  console.log(1);
  return undefined;
  console.log(2);
}

a() // 1

函数还会存在函数提升的概念,比如下面的代码,可以在定义函数之前调用函数

a() //  1

function a() {
  console.log(1);
}

// 相等于

function a() {
  console.log(1)
}

a() //  1

如果采用函数表达式赋值给变量,则函数无法提升,但是变量会提升

a() // 报错不是函数

var a = function() {
  console.log(1);
}


//  相等于

var a;
a()

a = function() {
  console.log()
}

函数的作用域

作用域主要指的是变量存在的范围,即变量能在哪些地方访问,ES5中规定了两个作用域,一个是全局作用域一个是函数作用域,其他都不是作用域,也就是说除了在函数作用域定义的变量都是属于全局作用域,即在整个程序中都可以进行访问,而函数作用域中定义的变量,只能在函数中访问。

var a = 1; //定义一个全局作用域下的变量

function b() {
  console.log(a) //访问全局变量
}

b() //  1

下面的代码展示了函数内定义的变量无法外部访问

function b() {
  var a = 1;
}

a // 报错 未定义

并且作用域内的变量具备变量提升的

function a() {
  console.log(b); // undefined 但未报错

  var b = 2;
}

//  相等于


function a() {
  var b;

  console.log(b);

  b = 2;
}

一定要注意在函数内部定义变量要使用var语句,不然定义的则是全局变量,如果全局变量中有相名字的变量则会覆盖全局变量

var a = 1;

function b() {
  a = 2;
  c = 3;
} 

b()

console.log(a); //  2 修改了全局变量

console.log(c); //  3 创建了全局变量,能够在全局范围内访问

函数的参数

函数定义参数在圆括号内定义参数,每个参数用逗号分隔

function a(b, c) {
  console.log(b);
  console.log(c);
}

a(1, 2)
//  1
//  2

Javascript中的函数并不是必须的,如果不传入参数也是合法的,因传入的参数是等于undefined

function a(b, c) {
  console.log(b);
  console.log(c);
}

a()
// undefined
// undefined

函数的传递方式是按值传递的,原始值是直接复制一份的,所以在函数体内部操作原始值是不影响外部的

var b = 1;

function a(b) {
  b = 2;
  console.log(b);
}

a(b) // 2

b   // 1

但是如果是对象类型,也是可以传递的,正如前面对象章节我所说的

但如果变量中存储的是对象,可以想象为是存储的值为一个内存地址,这个内容地址指向的是一个对象,所以如果多个变量之间都是这个内存地址,那么多个变量都是指向的同一个对象,如果对其中的一个变量的值进行重新赋值,那么实际上你修改的是变量中存储的内存地址而并不影响这个对象,但如果你通过方括号[]运算符赋值、点.运算符赋值,实际上是对这个变量值中内存地址指向的对象进行赋值,所以影响的是所有指向这个对象的变量

所以可以理解函数的参数都是按照值传递的,原始值就是按照拷贝一份原始值传递,而对象也是按照拷贝一份原始值传递的。

如果参数中出现了同名参数,那么在内部使用的时候,实际上是取最后传递进来的那个同名参数

function a(b, b) {
  console.log(b);
}

a(1, 2) // 2

arguments对象

由于Javascript对于参数的机制,比如可以定义参数但不传入参数等情况,那么这样的情况需要一种机制来处理,那么就有了arguments对象,该对象是函数的一个局部变量,这个对象的length属性可以检测传入了多少个参数

function a(b, b, b) {
  console.log(arguments.length);
}

a(1, 2, 3) // 3

同时arguments对象可以接收未经定义的参数

function a() {
  console.log(arguments.length);
}

a(1, 2) // 2

通过[]方括号可以访问每个参数,如果超出了参数的数量范围则返回undefined

function a() {
  console.log(arguments[1]);
  console.log(arguments[2]);
}

a('a', 'b') 
// b
// undefined

arguments对象不数组,是一种类似的数组,除了具备[]索引和length属性之外,没有任何数组的特性,下面的代码能够检测出不是数组,并且arguments对象是一个对象类型,但是可以通过数组的slicefrom方法转换为数组

function a() {
  console.log(arguments instanceof Array);

  console.log(Object.prototype.toString(arguments));
}

a() 
// false
//  [object Object]

arguments对象还有一个callee属性标示自身,这样就可以通过这个属性实现自身迭代,但是该属性严格模式下是禁用的

function a() {
  console.log(arguments.callee === a);
}

a() // true

函数的属性和方法

函数的name属性是返回函数的名字,如果是通过表达式赋值的则返回变量的名字

function a() {}

a.name // a

//表达式函数

var a = function() {}

a.name // a

但是通过表达式赋值给变量的时候同时也定义了函数的名字则返回定义的名字

var a = function b() {}

a.name // b

函数的length属性返回预期参数的数量,则定义参数的数量,这里只返回定义的参数数量而不是传入参数的数量

function a(a, b) {}

a.length // 2

函数的toString方法返回函数内部的函数体源码,包括注释也会返回

function a() {
  console.log(1);
  // 注释
}

a.toString()
//"function a() {
//  console.log(1);
//  // 注释
//}"

闭包

在Js中经常会听到闭包这个词,闭包也是Js的特色,了解闭包首先得知道全局作用域和函数作用域,在上面函数部分提到过,都知道作用域是指变量存在的范围,而Js中的函数作用域可以范围全局作用域的变量

var a = 1;

function b() {
  console.log(a);
}

b() //  1

那如果在b函数内在定义一个c函数,那么此时的c函数可以访问b函数的作用域内的变量以及全局作用域的变量

var a = 2;

function f() {
  var b = 4;

  console.log(a); // 2

  function x() {
    console.log(a); // 2 访问全局变量
    console.log(b); // 4 访问b函数的局部变量
  }

}

上面的代码函数内访问变量是属于链式查找,比如当前函数作用域没有这个变量,那么就继续查找包裹他的函数有不有这个变量,一直查找到全局作用域,而上面的代码中f函数不能够访问x函数,因为链式查找只能从下到上,从内到外,而不能反向,那么可以将上面的代码想象成下面这样

全局作用域{

  函数f作用域{

    函数x作用域{

    }

  }

}

而闭包就是利用了这样的特性,都知道一个函数内部的变量是无法在外部进行访问、赋值的,那么在内部再定义一个函数,然后将此函数作为返回值返回,不就可以操作这个函数内的变量了吗?

function f() {
  var a = 1;
  return function x() {
    console.log(a);
  }
}

var fx = f();

fx() // 1

上面的代码中x函数就是闭包,闭包就是将函数内部与函数外部连接起来的桥梁。

立即调用函数表达式(IIFE)

Javascript中圆括号是一种运算符,跟在函数后面表示调用该函数,比如上面代码中的fx()表示调用fx函数

那么可以在定义函数后就调用函数

function(){}() // 报错

上面代码之所以报错,是因为function这个关键字是可以定义函数,也可以作为函数表达式,而当作用函数表达式是没有问题的,但是作为定义函数的语句就回产生问题了,因为定义函数语句可能像下面这样

function() {} ()

// 因为函数提升,可能会出现这样的情况,所以引擎识别很有问题

function(){}
() 

所以为了避免这样的歧义,Javascript引擎规定,function关键字出现在行首,一律解释成语句。

那如果需要使用立即调用,那就需要避免在行首使用function关键字,最简单的方式是使用()圆括号包裹

(function(){console.log(1)}()) // 1

只要能避免行首使用function关键字都可以立即调用

比如下面的代码

+function(){console.log(1)}()
-function(){console.log(1)}()
~function(){console.log(1)}()
!function(){console.log(1)}()

true && function(){console.log(1)}()
false || function(){console.log(1)}()
0, false || function(){console.log(1)}()

//或者是函数表达式

var f = function(){console.log(1)}() 

参考链接

《JavaScript 教程》 – 阮一峰

《Speaking JavaScript》 – Dr. Axel Rauschmayer

《JavaScript语言精粹》 – Douglas Crockford

HTTP URL 字符转义 字符编码 、 RFC 3986编码规范 – panchanggui

该死的IEEE-754浮点数,说「约」就「约」,你的底线呢?以JS的名义来好好查查你 – StinsonZhao

Object – MDN

Arguments 对象 – MDN

类型转换

Javascript是一个动态类型的语言,所有的变量不需要像其他语言一样声明变量是什么类型的,所以可以随意给变量赋值,但正是因为这样的情况,导致了很多值只能在程序运行的阶段才知道到底是什么值,那么检查类型和类型转换就是必须知道的一个内容。

检查数据类型

JavaScript种有三种方法可以检查数据是什么类型的,分别为:

  1. typeof运算符
  2. instanceof运算符
  3. Object.prototype.toString方法

typeof运算符可以检查所有的数据类型,并返回一个所属数据数据类型的字符串,但是因为上面提到函数、数组、对象是属于对象类型,所以实际上typeof运算符是将函数、数组、对象均看作是对象类型,返回的也是一样。

typeof 'javascript' // 'string'
typeof 1 // 'number'
typeof true // 'boolean'
typeof undefined // 'undefined'

typeof null //'object' 这是因为null表示的是一个空对象,所以返回的是object

typeof {} // 'object' 对象返回的是object
typeof [] // 'object' 数组返回的是'object'

function a(){}

typeof a // 'object' 函数返回的同样是object

null的类型是object,这是由于历史原因造成的。1995年的 JavaScript 语言第一版,只设计了五种数据类型(对象、整数、浮点数、字符串和布尔值),没考虑null,只把它当作object的一种特殊值。后来null独立出来,作为一种单独的数据类型,为了兼容以前的代码,typeof null返回object就没法改变了。

instanceof运算符只能针对于对象类型的检测,如果试图传入一个原始值将会得到错误,instanceof运算符可以检查所有的对象类型,比如函数、数组、对象等,但是函数、数组、对象本身也属于数据类型,所以当用这三种对象类型去检查是否为Object类型的时候,也是返回的true

var a = {}; //初始化一个变量并赋值空对象给这个变量

a instanceof Object // true

function a(){}

a instanceof Object // true

var a = [];

a instanceof Object // true

但是针对于函数、数组、对象特定的来检查这样是没有问题的

var a = {};

a instanceof Object // true

var a = [];

a instanceof Array //true

function a() {}

a instanceof Function // true

但是这里是着很大的问题,因为当如果只允许传入对象的时候,这时通过typeofinstanceof来检查是否传入是否为对象,那么如果一旦传入的是函数或者数组都会造成等于对象。

如果需要完美的解决所有的类型,可以采用Object.prototype.toString方法,因为它能完美的返回所有的类型,包括null

不同数据类型的Object.prototype.toString方法返回值如下:

  • 数值:返回[object Number]
  • 字符串:返回[object String]
  • 布尔值:返回[object Boolean]
  • undefined:返回[object Undefined]
  • null:返回[object Null]
  • 数组:返回[object Array]
  • arguments 对象:返回[object Arguments]
  • 函数:返回[object Function]
  • Error 对象:返回[object Error]
  • Date 对象:返回[object Date]
  • RegExp 对象:返回[object RegExp]
  • 其他对象:返回[object Object]

可以从上面看出来,所有的值都能够完美的检查出来,并返回[object object]这样的字符串,那为什么会有两个object字符串呢?

因为在ECMAScript5.1中规定这个方法返回由三个字符串"[object ", class, 以及 "]"连接组成,而这个class变量则是等于内部属性[[Class]]

所有内置对象中的这个[[Class]]由官方定义是什么值,官方这样描述这个值是什么:一个字符串值,表明了该对象的类型

在使用Object.prototype.toString方法的时候还需要调用call方法,是call方法在后面的内容中会讲解到,现在只需要知道这样的方式可以对完美检查所有类型的值。

Object.prototype.toString.call('ja') //`[object String]`

Object.prototype.toString.call([]) //'[object Array]'

转换

在数据类型一章节中,所有的原始值都对应了一个构造函数,可以通过这个构造函数来进行创建,那么相当于有两种方式可以进行创建原始值,但是转换又分为两种,分别为:

  • 强制转换
  • 自动转换

强制转换

强制转换一般指的是将其他类型值转换为基本类型的过程,一般强制转换的类型是字符串、布尔值、数值,对应有三个函数进行转换,分别为:

  • Number()
  • String()
  • Boolean()

Number函数

Number函数可以将任意值转换为数值类型的值,对于原始值的转换有以下几种规则:

  • 数值:直接返回该值
  • 字符串:如果被解析为数值,则转换为相应的值,比如'123'。如果字符串中出现了不能转换的值则返回NaN,比如'123d'
  • 空字符串和空值:转换为0
  • 布尔值:true转换为1false转换为0
  • undefined:转换为Nan
  • null:转换为0

Number函数对于对象的转换分为三步规则:

  1. 调用对象自身的valueOf方法,如果返回原始值则直接使用Number函数转换该值并返回
  2. 如果valueOf方法返回的还是对象,则改为调用对象自身的toString方法,如果toString方法返回原始值则直接使用Number函数转换该值并返回
  3. 如果toString方法返回的还是对象,则报错

String函数

String函数对于原始的转换有以下几种规则:

  • 数值:转换为相应的字符串,如123转换为'123'
  • 字符串:直接返回该值
  • 布尔值:true转换为'true'false转换为'false'
  • undefined:转换为'undefined'
  • null:转换为'null'

String函数对于对象的转换分为三步规则:

  1. 调用对象自身的toString方法,如果返回原始值则直接使用String函数转换该值并返回
  2. 如果toString方法返回的是对象,在调用对象的valueOf方法,如果该方法返回原始值则直接使用String函数转换该值并返回
  3. 如果valueOf返回的还是对象,则报错

Boolean函数

Boolean函数对于原始值的转换除了以下的五个值为false,其他都转换为true

  1. undefined
  2. null
  3. -0+0
  4. NaN
  5. 空字符串和空值

Boolean函数对于所有的对象都返回true

自动转换

自动转换是Javascript引擎对于预期位置的值进行转换,这个转换是基于强制转换为基础的,对于用户是不可见的,由于自动转换存在不确定性,所以建议在不能确定值类型的时候对值进行强制转换

一般自动转换为三种:

  1. 不同类型的数据互相运算:如123 + 'abc'返回'123abc'
  2. 对于非布尔值类型的数据求布尔值:如if('abc'),该判断为true,常见的在if语句中
  3. 对于非数值类型的值使用一元运算符+-:如+ {foo: 'bar'}返回NaN

自动转换为布尔值

Javascript预期为布尔值的地方,如ifwhile语句等,就会调用Boolean函数进行转换

if (undefined) {
    console.log(1); // 不会执行,因为规则boolean转换规则转换为false
}

自动转换为字符串

字符串自动转换的地方主要在字符串的加法运算的时候,当一个值为字符串,另一个值为非字符串,则后者转换为字符串,转换规则按照String函数规则转换

'10' + 1 //101

自动转换为数值

Javascript遇到预期为数值的地方都会转换为数值,除了+运算符可能会转换为数值之外,其他的运算符都会将值转换为数值进行运算,该转换规则按照Number函数的转换规则转换

'5' * '2' //10

参考链接

《数据类型的转换》 – 阮一峰

《Speaking JavaScript》 – Dr. Axel Rauschmayer

《JavaScript语言精粹》 – Douglas Crockford

JavaScript:Object.prototype.toString方法的原理 – 紫云飞

MDN – Mozilla Developer Network

算术运算符

Javascript提供了10种算术运算符,这些运算符减法、乘法、除法一些运算符比较简单,就是简单的数学运算,之后重点记录一些运算符,而不是全部介绍,这些运算符分别为:

  • 加法运算符x + y
  • 减法运算符x - y
  • 乘法运算符x * y
  • 除法运算符x / y
  • 指数运算符x ** y
  • 余数运算符x % y
  • 自增运算符++x 或者 x++
  • 自减运算符--x 或者 x--
  • 数值运算符+x
  • 负数值运算符-x

加减号运算符

加减号运算符最简单的还是用于数值的相加,比如

1 + 1 // 2

10 + 100 // 110 

2 - 1 //1

同时加法运算符也可以用于非数值的加减号运算,则转换规则按照类型转换一章的来说,比如true会被转换为数值1,下面的减法运算后等于0是因为true会被转换为数值1,所以结果为0

true + 1 // 2
true - 1 // 0

如果加法运算符是两个字符串,就会执行字符串相连的操作,然后返回一个新的字符串

'he' + 'llo' // hello

如果其中一个值是字符串,而其中另外一个值不是字符串,那么就会执行字符串相连的操作,非字符串的值会按照String的转换规则进行转换

'hello' + 10 //hello10

如果多个值进行加法运算的话,加法运算符是从到右符合数学的运算规律,那么每次从左到右操作两个值,则用上面的规则来进行相加

1 + 2 + 'ok' // '3ok'

比如上面的代码中,先一次操作是1 + 2,两边都为数值,直接相加,第二次操作为3 + 'ok',其中有一个值为字符串,那么则非字符串的值也会转为字符串,所以最后为'3ok'

如果与对象相加,那么会返回为字符串,因为根据转换的规则,如果两个值相加,其中一个值为字符串,那么就会将另一个值转换为字符串,比如

var a = {
    b: 1
};

1 + a
//"1[object Object]"

上面的代码中会分为两部进行执行:

1、调用对象自身的valueOf方法,返回原始值,然后进行字符串拼接

2、如果valueOf方法返回的不是原始值,那么调用对象的toString方法,然后进行字符串拼接

上面的代码中,其实最终对象a会被转换为”[object Object]”,那么既然其中一个是字符串,那么不管另外一个是什么值都会转换为字符串,所以数值1也会被转换为字符串'1',所以最终的结果就是"1[object Object]"

如果调用的是Date对象的实例,那么会优先执行toString方法,而执行的顺序就是上面的2-1这样转换。

数学运算符+-运算符用作数学运算它是二元运算符,即有两个操作值,如:

1 + 2
1 - 1

但是加号和减号运算符同样也是一元运算符,这两个运算符如果当作一元运算符放在值的前面相当于Number函数的作用,可以将运算符后面的值转换为数值,而转换的规则都是以Number函数的转换规则一致

+[] // 0
+true // 1
+false // 0

减号运算符结果同样也是一样的,但是加号运算符返回的是正号的数值,而减号运算符返回的是带负号的数值

-[] // -0
-true // -1
-false // -0

一元加减的运算符返回的是一个新值,并不会影响原先变量的值,所以后面调用a返回的值还是原来的值

var a = [];
+a // 0

a // []

余数运算符

余数运算符%,返回前一个数被后一个数除后的余数。

22 % 4 // 2

余数运算符返回值是正数还是负数由第一个值的符号决定,比如第一个是正数,那么结果永远为正数,如果第一个为负数,那么结果永远为负数

-22 % 4 // -2
22 % -4 // 2

除此之外,余数运算符还可以用于浮点运算符,但是因为之前在数据类型一章说过,浮点数并不是精准的,所以无法返回准确的值。

自增和自减运算符

自增和自减预算符由两个减号构成,作用是将操作的值先用转换规则转换为数值,然后再进行自增数值1和自减数值1,并且会修改变量的值,它们都有两种方式,一种为前置--a,一种为后置a--

var a = 2;

--a // 1

a // 1 

var b = 2;

b-- //2

b   //1

从上面的代码可以看出来,实际上这两种方式最终的结果都是一样的,但是唯一不一样的区别在于,前置是先减去1然后再返回变量的值,所以从上面的变量a我们能够看出来,执行了自减后立马返回了1

而后面的变量b,用后置自减后是先将变量的值返回,然后再减去1,所以当时返回的是2,但是后面调用变量的时候是1,这样的情况自增和自减都是一样的。

指数运算符

指数运算符用数学符号表示为底数^{指数},在Js中由两个星号组成**,这是一个二元运算符,即有两个值,前一个值为底数,后一个值为指数

2**8 //256

在JS中,指数运算符是从右边开始计算的,比如下面的计算,实际上等于2^{2 * 2}

2 ** 2 ** 2 // 16
//实际上 上面的计算顺序为
2 ** (2 ** 2)

比较运算符

比较运算符的作用是将两个值进行比较大小,然后返回一个布尔值,而Js提供的比较运算符一共有8种,分别为:

  • > 大于运算符
  • < 小于运算符
  • <= 小于或等于运算符
  • >= 大于或等于运算符
  • == 相等运算符
  • === 严格相等运算符
  • != 不相等运算符
  • !== 严格不相等运算符

这8种比较运算符分为两类为相等比较非相等比较,其中的相等比较为:

  • == 相等运算符
  • === 严格相等运算符
  • != 不相等运算符
  • !== 严格不相等运算符

其中的非相等比较为:

  • > 大于运算符
  • < 小于运算符
  • <= 小于或等于运算符
  • >= 大于或等于运算符

并且需要注意的是,任何的值与NaN进行比较,都是返回的为false,包括非相等比较和相等比较的所有运算符,而-0+0是相等的。

非相等比较

非相等比较,一般会进行下面的尝试:

  • 如果两边的值为字符串,对比Unicode码的大小

  • 如果不为字符串,则将其两个值转换为数值,再进行比较

  • 如果有对象为运算子,则将该对象转换为原始值进行比较

如字符c的Unicode码为99,而C的Unicode码为69,所以对于非相等比较来说,比较的是Unicode的大小,所以这里大写的C会比小写的c

'C' < 'c' // true

由于所有的Unicode字符串支持很多国家的字符,所有对于中文来说也是支持的

非相等比较对于非字符串的值进行比较的时候会叫其值先转换为数值,然后再进行比较

4 < '5' //true
//实际上上面的代码等于
4 < Number(5) //true

2 > true // true
// 等同于
2 > Number(true) // true

如果运算子为对象,那么想会调用该对象的valueOf方法,如果该方法返回的还是一个对象,那么继续调用该对象的toString方法,然后再按照上面的规则进行比较。

var a = [2];

a > '1' // true

两个对象之间的非相等比较也是这样进行的

[2] > [1] // true
//等同于
[2].valueOf().toString() > [1].valueOf().toString()

严格相等运算符与严格不相等运算符

JS提供了两种相等运算符,一种为相等运算符==、一种为严格相等运算符===,这两个相等运算符的区别在于,相等运算符比较的是两个值是否相等,而严格相等运算符比较的则是两个值是否为同一个值

或者可以直接说成相等运算符会对值进行转换成同一类型,而严格相等运算符则不会进行类型的转换。

严格相等运算符对于不同类型的值会直接返回false

1 === '1' //false
true === 'true' // false

严格相等运算符对于同类型的原始值会进行值比较,如果值相同就返回true,否则返回false

1 === 1 //true
true === true //true
-0 === +0 //true

对于对象类型的值进行比较,严格相等运算符是直接比较的内存地址,也就是比较的这两个变量引用的是不是同一个对象类型的值

{} === {} //两个独立的对象 false

var a = [];
var b = a;

a === b // true

严格等于对于null以及undefined这两个值,用自身严格相等于自身是相等的

null === null //true
undefined === undefined // true

严格不相等通过运算符!==进行的,原理是基于严格相等的结果然后进行取反,所以当运算符!也可以来进行取反运算

1 !== 1 //false
1 !== 2 //true

!true // false
!false // true
//取反运算

相等运算符与不相等运算符

相等运算符用来比较相同类型的数据时,与严格相等运算符完全一样。

1 == 1.0
// 等同于
1 === 1.0

比较不同类型的数据时,相等运算符会先将数据进行类型转换,然后再用严格相等运算符比较。下面分成三种情况,讨论不同类型的值互相比较的规则。

(1)原始值相等比较

原始值相等比较会将原始值转换为数值,然后再进行比较,转换规则为类型转换一章节里面的Number函数转换的规则一致

1 == true //true
//等同于
1 == Number(true) //true

1 == '1' //true
//等同于
1 == Number('1') //true

(2)对象类型的值与原始值比较

与对象类型的值进行比较的时候,会先看另一边的运算子是什么原始值然后再进行转换为相应的值进行比较:

  • 对象与数值比较:对象转换为数值,按照Number规则进行转换
  • 对象与字符串比较:对象转换为字符串,按照String规则进行转换
  • 对象与布尔值进行比较:两边的都转换为数值,按照Number规则进行转换

(3)null与undefined

相等对于null以及undefined这两个值,用自身相等于自身是相等的,并且这两个值互相比较是相等的

null == null //true
undefined == undefined // true

null == undefined //true

而这两个值与其他值进行比较的时候都是不相等的

false == null //false
0 == undefined // false

0 == null //false

相等运算符隐藏的类型转换,会带来一些违反直觉的结果。

0 == ''             // true
0 == '0'            // true

2 == true           // false
2 == false          // false

false == 'false'    // false
false == '0'        // true

false == undefined  // false
false == null       // false
null == undefined   // true

' \t\r\n ' == 0     // true

上面这些表达式都不同于直觉,很容易出错。因此建议不要使用相等运算符(==),最好只使用严格相等运算符(===)。

不相等通过运算符!==进行的,原理是基于相等的结果然后进行取反,所以当运算符!也可以来进行取反运算

1 != 1 //false
1 != 2 //true

!true // false
!false // true
//取反运算

布尔运算符

Js提供了四个布尔值运算符,用于将表达式转换为布尔值,分别为:

  • 取反运算符:!
  • 且运算符:&&
  • 或运算符:||
  • 三元运算符:?:

取反运算符

取反运算符可以将布尔值取反,即可以将true转换为false

!true //false

取反运算符对于非布尔值,除了以下的6种会转换为true,其余的都转换为false,这6种情况分别为:

  • undefined
  • null
  • false
  • 0
  • NaN
  • ''空字符串

且运算符

且运算符&&是一个二元运算符,会返回其中一个运算子的值,需要注意的是这里的返回的是值,而不是布尔值,且运算符的运算规则为:

  • 第一个运算子的值为true则返回第二个运算子的值
  • 第一个运算子的值为false则返回第一个运算子的值,则不再对第二个运算子求值

上面的第二种的可以跳过第二个运算子的规则,被称为短路,这样的方式可以来代替很多语句,如if等判断语句,而这个的运算规则按照Boolean函数的规则来转换

function a (b) {
    return b && 'This is true';
};

比如上面的函数,判断用户输入是否为true,如果传入的值可以转换的为true则返回This is true字符串,如果传入的值转换为false那么就返回传入的那个值。

或运算符

或运算符||是一个二元运算符,会返回其中一个运算子的值,需要注意的是这里的返回的是值,而不是布尔值,或运算符的运算规则为:

  • 第一个运算子的值为true则返回第一个运算子的值,则不再对第二个运算子求值
  • 第一个运算子的值为false则返回第二个运算子的值

上面的第一种的可以跳过第二个运算子的规则,同样被称为短路,与且运算符相似

三元运算符

Js只有一个三元运算符,即由一个?和一个:组成,三元运算符?:的规则为:

  • 如果第一个运算子的值转换为true则返回第二个运算子的值
  • 如果第一个运算子的值转换为false则返回第三个运算子的值

三元运算符?:同样可以来代替简单的if语句,只建议用来代替简单的if语句。

if( a ) {
    return 'This is true';
} else {
    return 'This is false';
}

//等同于
a ? 'This is true' : 'This is false'

二进制位运算符

二进制位运算符只能用作整数,如果不是整数会自动转换为正数再执行,虽然在Javascript中所有数值都是以双精确数64位的浮点数存放的,但是在运算过程中会转换为32位带符号的整数进行运算。

这就意味着如果无法转换为整数,那么就无法做二进制位运算并返回0,并且如果超出了32位整数的最大值就会造成无法预测的值返回。

Js提供了7个可以用来计算、操作二进制的运算符,这分别为:

  • 二进制或运算符(or):符号为|,表示若两个二进制位都为0,则结果为0,否则为1
  • 二进制与运算符(and):符号为&,表示若两个二进制位都为1,则结果为1,否则为0
  • 二进取反运算符(not):符号为~,表示对一个二进制位取反。
  • 异或运算符(xor):符号为^,表示若两个二进制位不相同,则结果为1,否则为0
  • 左移运算符(left shift):符号为<<,详见下文解释。
  • 右移运算符(right shift):符号为>>,详见下文解释。
  • 带符号位的右移运算符(zero filled right shift):符号为>>>,详见下文解释。

二进制或运算符

二进制位或运算符号用|表示,将数值的二进制位相应的每个位进行比对,如果都为0则为0,其他则为1,比如下面的有两个数值,一个为1一个为2,经过或运算后得到的为3。下面用8位演示了二进制位或运算计算这两个值的过程

00000001
|
00000010
--------------------------
00000011 //二进制转换为十进制为3

// 等同Js代码

1 | 2  // 3

二进制与运算符

二进制位与运算符号用&表示,将数值的二进制位相应的每个位进行比对,如果都为1则为1,其他则为0,比如下面的有两个数值,一个为1一个为2,经过或运算后得到的为0。下面用8位演示了二进制位与运算计算这两个值的过程

00000001
&
00000010
--------------------------
00000000 //二进制转换为十进制为0

// 等同Js代码

1 & 2  // 0

二进制取反运算符

二进制取反运算符用~表示,可以对整数的二进制位进行取反,下面用8位演示了二进制位取反运算的过程

~00000001
----------------
 11111110

// 等同Js代码

~1 //-2 负数需要补码,所以将负数取反后加1,即得到的这个值


异或运算符

异或运算符用^表示,将数值的二进制位相应的每个位进行比对,如果对应位相同则为0,不相同则为1,下面用8位演示了二进制位取反运算的过程

00000001
^
00000010
--------------
00000011 // 3

// 等同Js代码

1 ^ 2 // 3


“异或运算”有一个特殊运用,连续对两个数ab进行三次异或运算,a^=b; b^=a; a^=b;,可以互换它们的值。这意味着,使用“异或运算”可以在不引入临时变量的前提下,互换两个变量的值。

var a = 10;
var b = 99;

a ^= b, b ^= a, a ^= b;

a // 99
b // 10

异或运算也可以用来取整

12.9 ^ 0 // 12

左移运算符

左移运算符用<<表示,左移运算符将一个整数的二进制位向左移动指定的位数,尾部用0补充,相当于用该值乘以2的次方,而次方则为移动的位数,即值 \times 2^{位数},下面用8位演示了二进制位左移运算的过程

00000010 << 1 // 2

// 等同Js代码

2 * 2 * 1

// 左移0位相当于取该数为整数,这是因为首先二进制位运算符会将浮点数转换为整数
2.2 << 0  // 2

右移运算符

右移运算符用>>表示,右移运算符将一个整数的二进制位向右移动指定的位数,头部用0补充,相当于用该值除以以2的次方,而次方则为移动的位数,即值 \div 2^{位数},下面用8位演示了二进制位右移运算的过程

00000010 >> 1 // 1

// 等同Js代码

2 / 2 * 1

// 右移0位相当于取该数为整数,这是因为首先二进制位运算符会将浮点数转换为整数
2.2 >> 0  // 2

带符号位的右移运算符

带符号位的右移运算符用>>>表示,将一个整数的二进制位向右移动指定的位数,包括符号也会一起移动,所以该带符号位的右移运算符得到的结果始终为正数,如果本身为正数没有什么影响和普通的右移运算符一直,而如果本身为负数,那么就会有很大的变化。

00000010 >>> 1 // 1  正数2带符号向右移动
10000010 >>> 1 // 2147483647 负数2带符号向右移动

其他运算符

除了上面的运算符之外,还有两种运算符分别为void运算符和,逗号运算符,下面会分别介绍这两种运算符

void运算符

void运算符的作用是执行一个表达式并返回一个undefined值,即不管表达式返回的是什么值都会返回undefined

void 1 // undefined
void a() //undefined

void有两种调用方式

void 1
void (1)

但是更建议的采用第二种方式,因为void运算符的优先性很高,所以如果遇见下面的代码就很有可能造成预期的错误值

void 1 + 2

//实际上为

(void 1) + 2

void运算符主要作用用于添加书签、以及防止超链或者提交表单跳转

<a href="javascript: void(f())">文字</a>

<a href="javascript: void(document.form.submit())">
  提交
</a>

逗号运算符

逗号,运算符用于对两个表达式求值,并返回后一个表达式的值

(1 + 2), (2 + 3)    //5

逗号,运算符一般用于在返回一个值之前进行一些辅助的操作,如:

var value = (console.log('Hi!'), true);
// Hi!

value // true

运算顺序

Javascript中正常的数学运算的运算顺序是符合数学逻辑的,比如

1 + 2 * 2

//等同于

1 + (2 * 2)

但是如果存在多个非数学运算的时候,就会导致很难理解,并且执行顺序很难理解

var x = 1;
var arr = [];

var y = arr.length <= 0 || arr[0] === undefined ? x : arr[0];

上面代码中,变量y的值就很难看出来,因为这个表达式涉及5个运算符,到底谁的优先级最高,实在不容易记住。

根据语言规格,这五个运算符的优先级从高到低依次为:小于等于(<=)、严格相等(===)、或(||)、三元(?:)、等号(=)。因此上面的表达式,实际的运算顺序如下。

var y = ((arr.length <= 0) || (arr[0] === undefined)) ? x : arr[0];

记住所有运算符的优先级,是非常难的,也是没有必要的。

圆括号

在Javascript中()圆括号可以提高表达式的优先级,因为圆括号的优先级是最高的,比如

(1 + 2) * 3 //9

上面的代码会优先执行圆括号内的表达式,但是需要知道的是圆括号不是运算符,而是一种语法结构,一般()圆括号有两个用法,一个是调用函数,另外一个是表达式放在圆括号中提高运算的优先级。

()圆括号内只能放表达式,如果放语句就会报错

(var a = 1) // Uncaught SyntaxError: Unexpected token var

左结合与右结合

对于相同级别的运算,大多情况,都是从左边开始运算,这样的情况叫做运算符的左结合,如

1 + 2 + 3

上面的表达式会先对1+2进行求值,然后再用求到的值与3相加。

但是有少数的运算符是右结合,其中有=赋值运算符、?:三元运算符以及指数运算符**

var a = b = 2;
//等同于
var a = ( b = 2 );


//对于三元运算符也是一样的
var a = b ? 1 : c ? 2 : 3;
//等同于
var a = b ? 1 : (c ? 2 : 3);

//指数运算同样也是右结合
2 ** 2 ** 2
//等同于
2 ** (2 ** 2)

参考链接

《算术运算符》 – 阮一峰

《比较运算符》 – 阮一峰

Leave a Reply

Your email address will not be published. Required fields are marked *