JavaScript高级程序设计 第三章 语言基础

第三章 语言基础

笔记基于JavaScript高级程序设计第四版,整理补充一些知识,以及一些自己的理解。

1. 变量

1.1 var声明和let声明

1.1.1 var声明提升

  • 在使用var关键字声明的时候变量会自动提升到函数作用域顶部
function foo(){
  
  console.log(age);
  
  var age = 11;
  
}

foo();//undefinded

上面这个代码是等价于下面的

function foo(){
 
  var age;
  
  console.log(age);
  
  age = 11;
  
}

foo();//undefinded

  • var不在函数体内一样被提前了
console.log(name);//undefined

var name = 11;

console.log(name);//11

1.1.2 let 和 var 的区别

声明范围的差别
  • let声明的范围是块作用域
  • var声明的范围是函数的作用域
function test() {
  
  if(true){
    var name = 'hhh';
    console.log(name);
  }
  
  console.log(name); //hhh
  
  if(true){
    let age = 11;
    console.log(age);
  }
  
  console.log(age);//Uncaught ReferenceError :age is not defined
 
}

test();

  • 在这个代码中,由于var声明的范围是函数的作用域,因此var声明会被提到前面等同于下面的代码,但是let声明的变量作用域只在代码块中,因此在代码块外面是没有定义的。
function test() {
  
  var name;

  console.log(name);//undefinded

  if(true){
    name = 'hhh';
    console.log(name);
  }

  // ....

}

冗余声明的区别
  • let不能重复声明
var name;
var name;

let age;
let age;

特殊情况:JavaScript引擎会记录用于变量声明的标识符及其所在的块作用域,因此嵌套使用相同的标识符不会报错,而这是因为同一个块中没有重复声明

let age = 30;
console.log(age); //30

if(true) {
	let age = 10;
	console.log(age);//10
}

console.log(age);//30

1.1.3 暂时性死区

console.log(name);//undefined
var name = 'matter';

console.log(age);//ReferenceError: Cannot access 'age' before initialization
let age = 22;

1.1.4 全局声明

var关键字不同,使用let关键字在e全局作用域中声明的变量不会成为windows对象的属性

var name = 'mattr';
console.log(windows.name);//mattr

let age = 12;
console.log(windows.age);//undefined

1.2 const声明

const的行为与let基本相同

区别:

  • 声明是必须要初始化变量
  • 修改const 声明的变量会导致运行时的错误

特殊情况:如果说const变量引用的是一个对象,那么修改这个对象的内部属性是可以的

const person = {};
person.name = 'Matt';

2. 数据类型

2.1 typeof 操作符

注意点:

  • typeof是操作符,不是函数!!
  • "object"表示值为对象或者是null
  • "function"表示值为函数
  • "symbol"表示值为符号
  • typeof null返回的是"object"。因为特殊值null被认为是一个对空对象的引用。

2.2 Undefined类型

undefined类型只有一个值,就是特殊值undefined

当使用var或者是let声明了变量但是没有初始化时,就相当于给变量赋予了undefined

一般来说我们没有必要显式的用undefined来初始化变量

  • 对于一个未声明的变量来说,只能执行一个有用的操作,就是使用typeof。(调用delete也不会报错,但是这个操作没有什么作用。)
let message;

console.log(message); //undefined
// console.log(test); //直接报错

console.log(typeof (message)); //undefined
console.log(typeof (test)); //undefined

当我们调用typeof返回一个没有声明的变量的时候所返回的值是undefined,和未初始化的变量所返回的结果是一致的。

注意 即使未初始化的变量会被自动赋予undefined值,但我们仍然建议在声明变量的同时进行初始化。这样,当typeof返回"undefined"时,你就会知道那是因为给定的变量尚未声明,而不是声明了但未初始化。

2.3 NUll类型

Null同样只有一个值null。从逻辑上讲,null值表示一个空对象指针,这也是给typeof传一个null会返回object的原因。

在定义将来要保存对象值的变量时,建议使用null来初始化,不使用其他的值,这样子只要检查这个变量的是不是null就可以知道这个变量是否在后来被重新赋予了一个对象的引用。这样就可以保持null是空对象指针的语义,并进一步将其与undefined区分开来

2.4 Boolean类型

布尔值,有两个字面量:truefalse

  • 这两个数值不用于数值
  • 字面量区别大小写,其他的形式是有效地标识符,但不是布尔值

Boolean()

虽然布尔值只有两个,但是其他的类型的值都有相应布尔值的等价形式。我们可以使用Boolean()转型函数

let test = 'hello';
console.log(Boolean(test)); //true

什么值能转换为truefalse的规则取决于数据类型和实际的值。下表总结了不同类型与布尔值之间的转换规则

数据类型 转换为true的值 转换为false的值
Boolean true false
String 非空字符串 ""(空字符串)
Number 非零数值(包括无穷值) 0NaN(参见后面的相关内容)
Object 任意对象 null
Undefined N/A(不存在) undefined

2.5 Number类型

2.5.1 整型

最基本的数值字面量格式是十进制整数,直接写出来就行。当然也可以用八进制或者十六进制字面量表示。

  • 八进制:数字开头必须是0
  • 十六进制:数字开头必须是0x
  • 当出现错误,比如说八进制中出现9,那么会直接忽略0,转而将其当成十进制
  • 八进制字面量在严格模式下是无效的。会导致JavaScript引擎抛出语法错误。
let intNum = 55;//十进制整数

let num1 = 070;//八进制的56
let num2 = 089;//无效的八进制 当做89处理
let num3 = 08;//无效的八进制	当做8处理

let num4 = 0xA;//十六进制的10
"use strict"

let num1 = 012;

console.log(num1);//SyntaxError: Octal literals are not allowed in strict mode.

1ECMAScript 2015或ES6中的八进制值通过前缀0o来表示;严格模式下,前缀0会被视为语法错误,如果要表示八进制值,应该使用前缀0o。——译者注

  • 使用八进制和十六进制格式创建的数值在所有数学操作中都视为十进制数值
let num1 = 10;
let num2 = 0o7;

console.log(num1 + num2);//17
console.log(num1 + num3);//20

2.5.2 浮点型

浮点型的数值,必须要有小数点出现。

  • let num1 = .1也是合法的,但是不推荐这么做。
  • 浮点数值使用的内存空间是整数的两倍,因此ECMAScript会想方设法的将数值转换为整数,在小数点后面没有数字的情况下,或者是(1.0)这种情况下,数值会自动的转换为整数。
科学计数法
let floatNum = 3.125e7; // 31250000
let floatNum2 = 3e-7; // 0.0000003

翻译成数学的科学计数法就应该是 \(3.125 \times 10^7\)\(3 \times 10^{-7}\)

  • 默认情况下ECMAScript会将小数点后至少包含6个零的浮点数值转换为科学计数法
  • 浮点数的精确度最高可达17位小数
特殊情况

浮点数在算术计算中远远不如整数精确

let a = 0.1;
let b = 0.2;

if (a + b == 0.3) {
    console.log("nice");//不会运行
}

console.log(a + b);//0.30000000000000004

注意 之所以存在这种舍入错误,是因为使用了IEEE 754数值,这种错误并非ECMAScript所独有。其他使用相同格式的语言也有这个问题。

2.5.3 值的范围

由于内存的限制,ECMAScript并不支持表示所有的数值。

  • Number.MIN_VALUE存放ECMAScript所能表示的最小数值
  • Number.MAX_VALUE存放ECMAScript所能表示的最大数值

如果得出的结果超过了JavaScript可以表示IDE范围,那么这个数值会被自动的转换为一个特殊的Infinity(无穷)值。

  • Infinity 正无穷,任何无法表示出来的正数。
  • -Infinity负无穷,任何无法表示出来的负数。

无穷并不能参与计算,无穷没有可以用于计算的数值表示形式。

isFinite()

用于判断一个值是不是有限大的

let result = Number.MAX_VALUE + Number.MAX_VALUE;
console.log(isFinite(result));  // false

注意 使用Number.NEGATIVE_INFINITYNumber.POSITIVE_INFINITY也可以获取正、负Infinity。没错,这两个属性包含的值分别就是-InfinityInfinity

2.5.4 NaN

NaN即为Not a Number。用于表示本来要返回数值的操作失败了(不是抛出错误)。

比如:用0除任意数值在其他的语言中通常会导致错误,从而中止代码执行,但在ECMAScript中,0、+0或-0相除会返回NaN

let num = 0;
let num2 = -0;
let num3 = +0;

console.log(num / num); //NaN
console.log(num2 / num3);   //NaN

console.log(1 / num);   //Infinity
console.log(1 / num2);  //-Infinity
console.log(1 / num3);  //Infinity

console.log(num / 1);   //0
console.log(num2 / 1);  //-0
console.log(num3 / 1);  //0
NaN几个独特的属性
  • 任何涉及NaN的操作始终返回NaN
  • NaN不等于包括NaN在内的任何值。
console.log(NaN / 12);  //NaN

if (NaN == NaN) {
    console.log('nice');    //不会执行
}
isNaN( )

接收一个任意类型的参数,然后判断这个值是否“不是数值”。把一个值传给isNaN后,该函数会尝试把他转换为数值。某些非数值的值可以直接转换成数值。任何不能转换为数值的值都会导致这个函数返回true

console.log(isNaN(NaN));     // true
console.log(isNaN(10));      // false,10是数值
console.log(isNaN("10"));    // false,可以转换为数值10
console.log(isNaN("blue"));  // true,不可以转换为数值
console.log(isNaN(true));    // false,可以转换为数值1

注意 虽然不常见,但isNaN()可以用于测试对象。此时,首先会调用对象的valueOf()方法,然后再确定返回的值是否可以转换为数值。如果不能,再调用toString()方法,并测试其返回值。这通常是ECMAScript内置函数和操作符的工作方式,本章后面会讨论。

2.5.5 数值转换

有三个函数可以将非数值转换为数值

  • Number()
  • parseInt()
  • `parseFloat()``

第一个是转型函数,可以用于任何数据类型。后两个函数主要用于将字符串转换为数值。

Number()函数

转换规则:

  • 布尔值,true转换为1,false转换为0。
  • 数值,直接返回。
  • null,返回0。
  • undefined,返回NaN
  • 字符串,应用以下规则。
    • 如果字符串包含数值字符,包括数值字符前面带加、减号的情况,则转换为一个十进制数值。因此,Number("1")返回1,Number("123")返回123,Number("011")返回11(忽略前面的零)。
    • 如果字符串包含有效的浮点值格式如"1.1",则会转换为相应的浮点值(同样,忽略前面的零)。
    • 如果字符串包含有效的十六进制格式如"0xf",则会转换为与该十六进制值对应的十进制整数值。
    • 如果是空字符串(不包含字符),则返回0。
    • 如果字符串包含除上述情况之外的其他字符,则返回NaN
  • 对象,调用valueOf()方法,并按照上述规则转换返回的值。如果转换结果是NaN,则调用toString()方法,再按照转换字符串的规则转换。
console.log(true);  //true
console.log(123);   //123
console.log(Number(null));  //0
console.log(Number(undefined)); //NaN
console.log(Number('011')); //11 忽略0
console.log(Number('+1'));  //1
console.log(Number('1.1')); //1.1
console.log(Number('0xA')); //10
console.log(Number('0o51')); //41 八进制
console.log(Number(''));    //0
console.log(Number('aa123'));   //NaN
parseInt()函数
  • 忽略字符串最前面的空格,从第一个非空格字符开始转换
  • 如果第一个字符不是数值字符、加号或减号,立即返回NaN
  • 空字符串也返回NaN
  • 如果第一个字符合法,继续监测后面的字符
    • 如果检测出非数值字符,非数值字符(包括小数点,加减号等等)后的内容将会忽略。
    • 若没有问题,检测到字符串末尾
  • 如果说字符串中的第一个字符是数值字符,那么parseInt()函数能识别不同的进制。换句话说,如果字符串以"0x"开头,就会被解释为十六进制整数。如果字符串以"0"开头,且紧跟着数值字符,在非严格模式下会被某些实现解释为八进制整数。
console.log(parseInt('123bb')); //123
console.log(parseInt(''));      //NaN
console.log(parseInt('0xA'));   //10 十六进制
console.log(parseInt('22.4'));  //22 小数点不是数值字符
console.log(parseInt('df70'));  //NaN  开头是非法字符
console.log(parseInt('   oxB'));//NaN 确实是空格,但是在表达其他进制的时候前面不能有空格

parseInt()也能接受第二个参数,用于指定底数(进制数)。

let num = parseInt('0xAA', 16);//170

//给了第二个参数的话,你也可以省略进制的前缀
let num2 = parseInt('AA', 16);//170
let num3 = parseInt('74', 8);//60
  • 不传底数(不给第二个参数)相当于让函数自己判断如何解析,因此,我们都加上就可以避免错误。
parseFloat()函数

大部分是和parseInt()一样,以下是不用的地方。

  • 始终忽略字符串开头的零,换句话说该函数无法转换非十进制格式,所以也不能指定底数。
  • 如果字符串表示整数,则返回整数
console.log(parseFloat('123ss'));   //123
console.log(parseFloat('0xA'));     //0
console.log(parseFloat('22.4'));    //22.4
console.log(parseFloat('1.0'));     //1
console.log(parseFloat('0123.123'));//123.123
console.log(parseFloat('Adf'));     //NaN

2.6 String类型

String(字符串)数据类型表示零或多个16位Unicode字符序列。字符串可以使用双引号(")、单引号(')或反引号(`)标示。

  • 在JavaScript中,文本数据被以字符串形式存储,单个字符没有单独的类型。
  • 字符串的内部格式始终是UTF-16,它不依赖与页面编码

2.6.1 字符字面量

字符串数据类型包含一些字符字面量,用于表示非打印字符或有其他用途的字符,如下表所示:

字面量 含义
\n 换行
\t 制表
\b 退格
\r 回车
\f 换页
\\ 反斜杠(\
\' 单引号('),在字符串以单引号标示时使用,例如'He said, \'hey.\''
\" 双引号("),在字符串以双引号标示时使用,例如"He said, \"hey.\""
``` 反引号(\``),在字符串以反引号标示时使用,例如``He said, \hey.```
\x*nn* 以十六进制编码*nn*表示的字符(其中*n*是十六进制数字0~F),例如\x41等于"A"
\u*nnnn* 以十六进制编码*nnnn*表示的Unicode字符(其中*n*是十六进制数字0~F),例如\u03a3等于希腊字符"Σ"
  • 使用转义序列只表示一个序列

字符串的长度可以通过其length属性获取

  • 如果字符串中包含双字节字符,那么length属性返回的值可能不是准确的字符数。第5章将具体讨论如何解决这个问题。

2.6.2 字符串的特点

  • 字符串本身是不可变的(immutable)。

如果需要消除某一个值,我们需要先销毁原来的字符串,然后再将包含新值的另一个字符串保存到该变量

let lang = 'java';
lang = lang + 'Script'
  • lang变量首先保存了字符串java
  • 然后lang被定义为,包含javaScript的字符串组合。
  • 此时,系统为变量分配一个能够容纳JavaScript这个字符串的内存空间
  • 然后将JavaScript这个字符串赋值到lang上。
  • 最后将内存空间内留存的 javaScript销毁。

以上的过程就是改变某一个字符串时发生的事情。这个过程是自动进行的,因此有些早期的浏览器在拼接字符串的时候非常慢。

2.6.3 转换为字符串

toString()方法

该函数返回当前值得字符串等价物

let age = 11;
console.log(age.toString());    //11

let found = true;
console.log(found.toString());  //true

let tase = null;
console.log(tase.toString());   //TypeError: Cannot read property 'toString' of null

toString()方法可见于数值(Number)、布尔值(Boolean)、对象(object)、字符串值(string)。nullundefined没有该方法。

  • 可以给toString()方法给一个参数,该参数作为底数。

    let num = 10;
    
    console.log(num.toString());
    console.log(num.toString(2));//以二进制的形式输出结果 1010
    console.log(num.toString(8));//以八进制的形式输出结果 12
    
String()函数

String()转型函数,始终返回表示相应类型值的字符串。

  • 如果我们不确定一个值是不是null或者undefined我们就使用该转型函数
  • 如果值有toString()方法,则调用该方法(不传参数)并返回结果。
  • 如果值是null,返回null
  • 如果值是undefined,返回undefined

2.6.4 模板字面量

模板字符串使用反引号 (` `) 来代替普通字符串中的用双引号和单引号。模板字符串可以包含特定语法(${expression})的占位符。占位符中的表达式和周围的文本会一起传递给一个默认函数,该函数负责将所有的部分连接起来,如果一个模板字符串由表达式开头,则该字符串被称为带标签的模板字符串,该表达式通常是一个函数,它会在模板字符串处理后被调用,在输出最终结果前,你都可以通过该函数来对模板字符串进行操作处理。在模版字符串内使用反引号(`)时,需要在它前面加转义符(\)。

—— MDN

模板字面量的特性——多行字符串

一般情况下:

let str = 'string text 1\n' + 
'string text 2'

console.log(str);
/*
string text 1
string text 2
*/

let str1 = 'string text 1\nstring text 2'
console.log(str1);
/* 
string text 1
string text 2
*/


let str2 = `string text 1 
string text 2`

console.log(str2);
/*
string text 1
string text 2
*/

let str3 = 'string text 1 ' +
    'string text 2';
console.log(str3);//string text 1 string text 2


由于模板字面量会保留反引号内部的空格,所以在使用的时候需要小心,格式正确的模板字符串常常看起来会缩进不当。

let str = `string text 1
           string text 2`;

console.log(str);
console.log(str.length);//38

这个模板字符串在string text 2 之前,在string text 1的换行符之后中间存在着很多的空格。因此计算str的长度的时候,有大问题。

模板字面量的特性——字符串插值

技术上讲,模板字面量不是字符串,而是一种特殊的JavaScript句法表达式,只不过求值后得到的是字符串。模板字面量在定义时立即求值并转换为字符串实例,任何插入的变量也会从它们最接近的作用域中取值。

字符串插值通过在${}中使用一个JavaScript表达式实现

let value = 11;
let exponent = 'second';

let interpolatedTemplateLiteral =
    `${value} to the ${exponent} power is ${value * value}`;

console.log(interpolatedTemplateLiteral);//11 to the second power is 121

所有插入的值都会使用toString()方法强制转换为字符串,而且任何JavaScript表达式都可以用于插值。嵌套的模板字符串不需要转义。

console.log(`Hello, ${ `World` }!`);  // Hello, World!复制代码
模板字面量的特性——标签函数

更高级的形式的模板字符串是带标签的模板字符串。标签使您可以用函数解析模板字符串。标签函数的第一个参数包含一个字符串值的数组。其余的参数与表达式相关。最后,你的函数可以返回处理好的的字符串(或者它可以返回完全不同的东西 )。用于该标签的函数的名称可以被命名为任何名字。

——MDN

标签函数的语法是函数名后面直接带一个模板字符串,并从模板字符串中的插值表达式中获取参数。

标签函数的第一个参数是被嵌入表达式分隔的文本的数组。第二个参数开始是嵌入表达式的内容。

——知乎

参考博客:https://blog.csdn.net/zhanglir333/article/details/78585435

标签函数可以自定义插值行为。

  • 标签函数会接收被插值记号(${})分隔后的东西。
  • 标签函数会接收对每个表达式求值的结果。
  • 标签函数本身是一个常规函数,通过前缀到模板字面量来应用自定义行为。

下面来看具体的例子

let a = 6;
let b = 9;

// 定义该标签函数
function simpleTag(strings, aExpression, bExpression, sum) {
    
    console.log(strings);//原始字符串数组 [ '', '+', '=', '' ]
    console.log(aExpression);//aExpression 6
    console.log(bExpression);//bExpresson 9
    console.log(sum);//sum 15

    return 'foobar';
}

let untaggedResult = `${a} + ${b} = ${a + b}`;
let taggedResult = simpleTag`${a} + ${b} = ${a + b}`;

console.log(untaggedResult);//6 + 9 = 15
console.log(taggedResult);//函数返回值 foobar

这里我整体上理解有点懵,我们再看一个来自于MDN的例子

let person = 'mike';
let age = 18;

function myTag(strings, personExp, ageExp){

    let str1 = strings[0];
    let str2 = strings[1];

    console.log(strings);//[ 'that ', ' is a ', '' ]

    // There is technically a string after
    // the final expression (in our example),
    // but it is empty (""), so disregard.
    // var str2 = strings[2];

    let ageStr;

    if (ageExp > 99) {
        ageStr = 'centenarian';
    } else {
        ageStr = 'youngster';
    }

    return str1 + personExp + str2 + ageStr;

}

let output = myTag`that ${person} is a ${age}`;

console.log(output);//that mike is a youngster
探究标签函数中的参数

参数中的第一个值,是一个字符数组。其中数组中的值是由${}作为分隔所分隔出来的值。举个例子

function myTag(strings, person, age) {
    
    console.log(strings);
    console.log(person);
    console.log(age);

}

let person = 'df';
let age = 12;

let output = myTag`that ${person} is a ${age}`;
  • 通过${}进行分隔,所以字符数组中的值应该是'that '' is a '

然后我们看看跑出来的结果

[ 'that ', ' is a ', '' ]
df
12

为什么这个时候最后面会多出一个''呢?

我们来看下面的例子:

function myTag(strings, person, age) {
    
    console.log(strings);
    console.log(person);
    console.log(age);

}

let person = 'df';
let age = 12;

let output = myTag`that ${person} is a ${age} ?`;

结果

[ 'that ', ' is a ', ' ?' ]
df
12

另一个例子

function myTag(strings, person, age) {
    
    console.log(strings);
    console.log(person);
    console.log(age);

}

let person = 'df';
let age = 12;

let output = myTag`${person} is a ${age} ?`;

结果:

[ '', ' is a ', ' ?' ]
df
12

通过上面的例子,找到了一个规律,就是当出现了莫名其妙的''的时候,是出现在${}在最前面或者是最后面的情况。换成分隔的理解就是,既然${}要将前后分离,那么当前面或者后面没有东西的时候,系统会自动为我们加上一个空字符来表示所谓的分隔。

通过上面的例子我们可以知道,对于一个有n个插值的模板字面量,传给标签函数的表达式参数的个数始终是n,而传给标签函数的第一个参数(strings)所包含的字符串的个数始终是n + 1。

因为表达式参数的数量是可变的,所以通常应该使用剩余操作符(rest operator)将他们收集到一个数组。

剩余参数语法允许我们将一个不定数量的参数表示为一个数组。

如果函数的最后一个命名参数以...为前缀,则它将成为一个由剩余参数组成的真数组,其中从0(包括)到theArgs.length(不包括)的元素由传递给函数的实际参数提供。

——MDN

let a = 5;
let b = 1;

function zipTag(strings, ...experssions) {
    
    console.log(strings);

    for (const experssion of experssions) {
        console.log(experssion);
    }

    return 'FOOBAR';
}

let untaggedResult = `${a} + ${b} = ${a + b}`;
let taggedResult = zipTag`${a} + ${b} = ${a + b}`;

console.log(untaggedResult);
console.log(taggedResult);
原始字符串

在标签函数的第一个参数中,存在一个特殊的属性raw ,我们可以通过它来访问模板字符串的原始字符串,而不经过特殊字符的替换。

function tag(strings) {
 console.log(strings.raw[0]);
}

tag`string text line 1 \n string text line2`;
//string text line 1 \n string text line2

另外,使用String.raw()方法创建原始字符串和使用默认模板和字符串连接创建是一样的。

let str = String.raw`Hi\nmeakle`;
let str2 = `Hi\nmeakle`;

console.log(str);//Hi\nmeakle
console.log(str2);
/*
Hi
meakle
*/

——MDN

我们可以再看看更加具体的例子

// Unicode示例
// \u00A9是版权符号
console.log(`\u00A9`);            // ©
console.log(String.raw`\u00A9`);  // \u00A9

// 换行符示例
console.log(`first line\nsecond line`);
// first line
// second line

console.log(String.raw`first line\nsecond line`); // "first line\nsecond line"

// 对实际的换行符来说是不行的
// 它们不会被转换成转义序列的形式
console.log(`first line
second line`);
// first line
// second line

console.log(String.raw`first line
second line`);
// first line
// second line

2.7 Symbol类型

Symbol(符号)是ECMAScript 6新增的数据类型。符号是原始值,且符号实例是唯一的、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。

符号就是用来创建唯一记号,进而用作非字符串形式的对象属性。

symbol 是一种基本数据类型 (primitive data type)。Symbol()函数会返回symbol类型的值,该类型具有静态属性和静态方法。它的静态属性会暴露几个内建的成员对象;它的静态方法会暴露全局的symbol注册,且类似于内建对象类,但作为构造函数来说它并不完整,因为它不支持语法:"new Symbol()"。

——MND

2.7.1 符号的基本使用

符号需要使用Symbol()函数初始化。因为符号本身也是原始类型。所以typeof操作符对符号返回symbol

let sym = Symbol();

console.log(typeof (sym));

稍微看了下后面的内容,感觉好多东西涉及到后面的知识了。再往后看几章回头再看。

2.8 Object类型

ECMAScript中的对象其实就是一组数据和功能的集合。对象通过new操作符后跟对象类型的名称来创建。开发者可以通过创建Object类型的实例来创建自己的对象,然后再给对象添加属性和方法:

let o = new Object();
  • ECMAScript中的Object也是派生其他对象的基类。Object类型的所有属性和方法在派生的对象上同样存在。

每个object实例都有如下属性和方法。

  • constructor:用于创建当前对象的函数。在前面的例子中,这个属性的值就是Object()函数。
  • hasOwnProperty(*propertyName*):用于判断当前对象实例(不是原型)上是否存在给定的属性。要检查的属性名必须是字符串(如o.hasOwnProperty("name"))或符号。
  • isPrototypeOf(*object*):用于判断当前对象是否为另一个对象的原型。(第8章将详细介绍原型。)
  • propertyIsEnumerable(*propertyName*):用于判断给定的属性是否可以使用(本章稍后讨论的)for-in语句枚举。与hasOwnProperty()一样,属性名必须是字符串。
  • toLocaleString():返回对象的字符串表示,该字符串反映对象所在的本地化执行环境。
  • toString():返回对象的字符串表示。
  • valueOf():返回对象对应的字符串、数值或布尔值表示。通常与toString()的返回值相同。

因为在ECMAScript中Object是所有对象的基类,所以任何对象都有这些属性和方法。第8章将介绍对象间的继承机制。

注意 严格来讲,ECMA-262中对象的行为不一定适合JavaScript中的其他对象。比如浏览器环境中的BOM和DOM对象,都是由宿主环境定义和提供的宿主对象。而宿主对象不受ECMA-262约束,所以它们可能会也可能不会继承Object

3. 操作符

操作符,用来操作数据。ECMAScript中的操作符能用于各种值,包括字符串,数值,布尔值,甚至还有对象。

JavaScript中的常见操作符

  • 数学操作符
  • 位操作符
  • 关系操作符
  • 相等操作符

3.1 一元操作符(unary operator)

一元操作符只操作一个值。

  1. 递增/递减操作符

    • 自增自减只能用于变量,用于常量将会报错

    • 前置形式和后置形式

      前置:先自加/自减、再返回值(返回新的值)

      后置:先返回值,再自加/自减(返回旧的值)

      //前置
      let count = 0;
      alert(++count);//打印出的值是1;
      
      //后置
      alert(count++);//打印出的值是1;
      
      alert(count);//此时打印出的值是2;
      
    • 优先级比绝大多数的运算符等级要高

    • 这四个操作符可以作用于任何值,但是要遵守以下规定

      • 对于字符串,如果是有效的数值形式,则转换为数值再应用改变。变量类型从字符串变成数值。
      • 对于字符串,如果不是有效的数值形式,则将变量的值设置为NaN。变量类型从字符串变成数值。
      • 对于布尔值,如果是false,则转换为0再应用改变。变量类型从布尔值变成数值。
      • 对于布尔值,如果是true,则转换为1再应用改变。变量类型从布尔值变成数值。
      • 对于浮点值,加1或减1。
      • 如果是对象,则调用其(第5章会详细介绍的)valueOf()方法取得可以操作的值。对得到的值应用上述规则。如果是NaN,则调用toString()并再次应用其他规则。变量类型从对象变成数值。
  2. 一元加和减

    一元加号(+)放在一个变量前面(+a):

    • 对于数值来说:没有任何变化

    • 对于非数值来说:

      执行与使用Number()转型函数一样的类型转换。

      • 布尔值,true转换为1,false转换为0。
      • 数值,直接返回。
      • null,返回0。
      • undefined,返回NaN
      • 字符串,应用以下规则。
      • 如果字符串包含数值字符,包括数值字符前面带加、减号的情况,则转换为一个十进制数值。因此,Number("1")返回1,Number("123")返回123,Number("011")返回11(忽略前面的零)。
      • 如果字符串包含有效的浮点值格式如"1.1",则会转换为相应的浮点值(同样,忽略前面的零)。
      • 如果字符串包含有效的十六进制格式如"0xf",则会转换为与该十六进制值对应的十进制整数值。
      • 如果是空字符串(不包含字符),则返回0。
      • 如果字符串包含除上述情况之外的其他字符,则返回NaN
      • 对象,调用valueOf()方法,并按照上述规则转换返回的值。如果转换结果是NaN,则调用toString()方法,再按照转换字符串的规则转换。

      —— 2.5.5 数值转换 Number()函数

    一元减号由一个减号(-)表示,放在变量前头,主要用于把数值变成负值。

    • 如果入到了非数值的情况,一元减号会执行和一元加号一样的操作,将目标值转换,然后再取负值。

3.2 位操作符

位操作符是操作内存中表示数据的比特(位)。ECMAScript中的所有数值都以IEEE 754 64位格式存储。

  • 位操作并不直接应用到64位上。
    1. 首先将数值转换为32位。
    2. 再进行位操作。
    3. 最后再将结果转换为64位。
  • 有符号整数使用32位前31位表示整数值。第32位表示数值的符号——符号位(sign bit)。
  • 负值以一种称为补码的二进制编码存储。一个数值的二进制补码通过如下步骤得出“
    1. 确定绝对值的二进制表示。
    2. 找到数值的反码,就是每一位都取反
    3. 将最后的结果加1。
  • 如果说位操作符应用到非数值,那么首先会使用Number()函数将该值转换为数值,然后再应用位操作。最终结果是数值。
  • 特殊值NaNInfinity在位操作中都会被当成0处理。

3.2.1 按位非

操作符号: ~ ,返回的是数值的补数。

  • 换句话说就是每一个都取反
let num1 = 25;
let num2 = ~num1;

console.log(num2);//-26
  • 按位非的效果就是对数值取反并减1。
  • 位操作的速度很快,比-num1 - 1速度快很多。

3.2.2 按位于

按位与操作符用和号(&)表示,有两个操作数。本质上,按位与就是将两个数的每一个位对齐,然后基于真值表中的规则,对每一位执行相应的与操作。

第一个数值的位 第二个数值的位 结果
1 1 1
1 0 0
0 1 0
0 0 0

按位与操作在两个位都是1时返回1,在任何一位是0时返回0。

3.2.3 按位或

按位或操作符用管道符(|)表示,同样有两个操作数。按位或遵循如下真值表:

第一个数值的位 第二个数值的位 结果
1 1 1
1 0 1
0 1 1
0 0 0

按位或操作在至少一位是1时返回1,两位都是0时返回0。

3.2.4. 按位异或

按位异或用脱字符(^)表示,同样有两个操作数。下面是按位异或的真值表:

第一个数的位 第二个数的位 结果
1 1 0
1 0 1
0 1 1
0 0 0

按位异或与按位或的区别是,它只在一位上是1的时候返回1(两位都是1或0,则返回0)。

  • 所谓异或换句话就是,两个位不同->返回真。

3.2.5. 左移

左移操作符用两个小于号(<<)表示(箭头指向哪里就往哪里移动),会按照指定的位数将数值的所有位向左移动。比如,如果数值2(二进制10)向左移5位,就会得到64(二进制1000000),如下所示:

let oldValue = 2;              // 等于二进制10
let newValue = oldValue << 5;  // 等于二进制1000000,即十进制64

注意在移位后,数值右端会空出5位。左移会以0填充这些空位,让结果是完整的32位数值(见图3-2)。

{%}

图 3-2

注意,左移会保留它所操作数值的符号。比如,如果-2左移5位,将得到-64,而不是正64。

3.2.6 有符号右移

有符号右移由两个大于号(>>)表示,会将数值的所有32位都向右移,同时保留符号(正或负)。有符号右移实际上是左移的逆运算。比如,如果将64右移5位,那就是2:

let oldValue = 64;             // 等于二进制1000000
let newValue = oldValue >> 5;  // 等于二进制10,即十进制2

同样,移位后就会出现空位。不过,右移后空位会出现在左侧,且在符号位之后(见图3-3)。ECMAScript会用符号位的值来填充这些空位,以得到完整的数值。

{%}

图 3-3

3.2.7 无符号右移

无符号右移用3个大于号表示(>>>),会将数值的所有32位都向右移。对于正数,无符号右移与有符号右移结果相同。仍然以前面有符号右移的例子为例,64向右移动5位,会变成2:

let oldValue = 64;              // 等于二进制1000000
let newValue = oldValue >>> 5;  // 等于二进制10,即十进制2

对于负数,有时候差异会非常大。与有符号右移不同,无符号右移会给空位补0,而不管符号位是什么。对正数来说,这跟有符号右移效果相同。但对负数来说,结果就差太多了。无符号右移操作符将负数的二进制表示当成正数的二进制表示来处理。因为负数是其绝对值的补码,所以右移之后结果变得非常之大,如下面的例子所示:

let oldValue = -64;              // 等于二进制11111111111111111111111111000000
let newValue = oldValue >>> 5;   // 等于十进制134217726

在对-64无符号右移5位后,结果是134 217 726。这是因为-64的二进制表示是11111111111111111111111111000000,无符号右移却将它当成正值,也就是4 294 967 232。把这个值右移5位后,结果是00000111111111111111111111111110,即134 217 726。

3.3 布尔操作符

布尔操作符一共有3个:

  • 逻辑非
  • 逻辑与
  • 逻辑或

3.3.1 逻辑非

逻辑非:! 以一个叹号表示。

  • 逻辑非始终返回一个布尔值。
  • 逻辑非可以应用给ECMAScript中的任何值。
    • 逻辑非先将操作数转换为布尔值,然后再对其取反。
  • 逻辑非遵循以下规则
    • 如果操作数是对象,则返回false
    • 如果操作数是空字符串,则返回true
    • 如果操作数是非空字符串,则返回false
    • 如果操作数是数值0,则返回true
    • 如果操作数是非0数值(包括Infinity),则返回false
    • 如果操作数是null,则返回true
    • 如果操作数是NaN,则返回true
    • 如果操作数是undefined,则返回true

对比:Boolean()转型函数所转换的值。

数据类型 转换为true的值 转换为false的值
Boolean true false
String 非空字符串 ""(空字符串)
Number 非零数值(包括无穷值) 0NaN(参见后面的相关内容)
Object 任意对象 null
Undefined N/A(不存在) undefined

通过上面的表格我们可以明显的得出。使用逻辑非之后得出的布尔值之后,我们再对其取逻辑非,这个时候的结果就和Boolean()转型函数的结果一样了。

逻辑非操作符也可以用于把任意值转换为布尔值。同时使用两个叹号(!!),相当于调用了转型函数Boolean()。无论操作数是什么类型,第一个叹号总会返回布尔值。第二个叹号对该布尔值取反,从而给出变量真正对应的布尔值。结果与对同一个值使用Boolean()函数是一样的。

console.log(!!"blue"); // true Boolean("blue");
console.log(!!0);      // false Boolean(0);
console.log(!!NaN);    // false ...
console.log(!!"");     // false ...
console.log(!!12345);  // true  ...

3.3.2 逻辑与

逻辑与操作符由两个和号(&&)表示,应用到两个值,如下所示:

let result = true && false;

逻辑与操作符遵循如下真值表:

第一个操作数 第二个操作数 结果
true true true
true false false
false true false
false false false

逻辑与操作符可用于任何类型的操作数,不限于布尔值。如果有操作数不是布尔值,则逻辑与并不一定会返回布尔值,而是遵循如下规则

  • 如果第一个操作数是对象,则返回第二个操作数。
  • 如果第二个操作数是对象,则只有第一个操作数求值为true才会返回该对象。
  • 如果两个操作数都是对象,则返回第二个操作数。
  • 如果有一个操作数是null,则返回null
  • 如果有一个操作数是NaN,则返回NaN
  • 如果有一个操作数是undefined,则返回undefined

逻辑与是一种短路操作符,意思就是第一个操作符决定了结果的话,那么永远不会对第二个操作数求值。

换句话来说就是,当第一个操作数是false,那么就不会计算后一个操作数,直接得出结果是false

let found = true;
let result = (found && someUndeclaredVariable); // 这里会出错
console.log(result); // 不会执行这一行

let found2 = false;
let result2 = (found2 && someUndeclaredVariable);//这里不会出错。因为执行不到

3.3.3 逻辑或

逻辑或操作符由两个管道符(||)表示,比如:

let result = true || false;

逻辑或操作符遵循如下真值表:

第一个操作数 第二个操作数 结果
true true true
true false true
false true true
false false false

与逻辑与类似,如果有一个操作数不是布尔值,那么逻辑或操作符也不一定返回布尔值。它遵循如下规则。(我建议直接记住补充里面的方法,里面可以解释以下规则的原因)

  • 如果第一个操作数是对象,则返回第一个操作数。
  • 如果第一个操作数求值为false,则返回第二个操作数。
  • 如果两个操作数都是对象,则返回第一个操作数。
  • 如果两个操作数都是null,则返回null
  • 如果两个操作数都是NaN,则返回NaN
  • 如果两个操作数都是undefined,则返回undefined

同样与逻辑与类似,逻辑或操作符也具有短路的特性。只不过对逻辑或而言,第一个操作数求值为true,第二个操作数就不会再被求值了。

3.3.4 补充 ——来自于JavaScript现代教程

||(或)

或运算,参与运算的任意一个值为true就返回true否则就是返回false。(操作数全为布尔类型的情况)

如果操作数并不是全是布尔类型,那么就会转换成布尔类型来参与运算。(此时的返回值不一定是布尔类型)

注意:如果参与运算的值是布尔类型的,逻辑运算符返回的是布尔类型的。如果不是,返回值应该是第一个真值的类型

具体分析在扩展情况里面。

** 扩展情况——或运算寻找第一个真值 **

result = value1 || value2 || value3;

或运算符 || 做了如下的事情:

  • 从左到右依次计算操作数。
  • 处理每一个操作数时,都将其转化为布尔值。如果结果是 true,就停止计算,返回这个操作数的初始值。
  • 如果所有的操作数都被计算过(也就是,转换结果都是 false),则返回最后一个操作数。

逻辑运算符返回的值是操作数的初始形式,不会做布尔转换。如果返回值是布尔类型那只能说参与操作的数有布尔类型

也就是,一个或运算 "||" 的链,将返回第一个真值,如果不存在真值,就返回该链的最后一个值。

ps:if(...)会将括号内的的值转换为布尔类型,所以说使用完逻辑运算符号,即使返回的值不是布尔值,也可以通过if语句转换

** 扩展用法1——获取变量列表或者表达式的第一个真值。**

例如,我们有 firstNamelastNamenickName 变量,都是可选的。

我们用或运算 || 来选择有值的那一个,并显示出来(如果没有设置,则用 匿名(anonymous)):

let firstName = "";
let lastName = "";
let nickName = "SuperCoder";

alert( firstName || lastName || nickName || "Anonymous"); // SuperCoder

如果所有变量都为假(falsy),结果就是 Anonymous

扩展用法2——短路求值(Short-circuit evaluation)

或运算符 || 的另一个特点是所谓的“短路求值”。

它的意思是,|| 对其参数进行处理,直到达到第一个真值,然后立即返回该值,而无需处理其他参数。

如果操作数不仅仅是一个值,而是一个有副作用的表达式,例如变量赋值或函数调用,那么这一特性的重要性就变得显而易见了。

在下面这个例子中,只会打印第二条信息:

true || alert("not printed");
false || alert("printed");

在第一行中,或运算符 || 在遇到 true 时立即停止运算,所以 alert 没有运行。

有时,人们利用这个特性,只在左侧的条件为假时才执行命令。

&&(与)

和或运算差不多

区别:

  1. 必须每一个参加与运算的元素都是true才能返回true(操作数全为布尔类型的情况)
  2. 与操作返回的值是第一个假值,而或操作返回的是第一个真值
  3. 与运算的优先级高于或运算

骚操作:使用&&来实现if

let x = 1;

(x > 0) && alert( 'Greater than zero!' );

&& 右边的代码只有运算抵达到那里才能被执行。也就是,当且仅当 (x > 0) 返回了真值。

所以我们基本可以类似地得到:

let x = 1;

if (x > 0) alert( 'Greater than zero!' );

虽然使用 && 写出的变体看起来更短,但 if 更明显,并且往往更具可读性。因此,我们建议根据每个语法结构的用途来使用:如果我们想要 if,就使用 if;如果我们想要逻辑与,就使用 &&

与或操作总结
  1. 说白了,这两个运算所返回值的类型是根据操作数的类型来定的
  2. 与运算返回值得逻辑是,返回第一个布尔值(该布尔值由操作数进行布尔转换得出)为的操作数(原来的类型,原来的数值)。如果你没有找到假的操作数,那就返回最后一个操作数。
  3. 或运算的返回值逻辑是,返回第一个布尔值(该布尔值由操作数进行布尔转换得出)为的操作数(原来的类型,原来的数值)。如果你没有找到真的操作数,那就返回最后一个操作数。
!(非)

感叹符号 ! 表示布尔非运算。

运算符接受一个参数,并按如下运作:

  1. 将操作数转化为布尔类型:true/false
  2. 返回相反的值。

两个非运算 !! 有时候用来将某个值转化为布尔类型:

alert( !!"non-empty string" ); // true
alert( !!null ); // false

也就是,第一个非运算将该值转化为布尔类型并取反,第二个非运算再次取反。最后我们就得到了一个任意值到布尔值的转化。

有更多详细的方法可以完成同样的事 —— 一个内置的 Boolean 函数:

alert( Boolean("non-empty string") ); // true
alert( Boolean(null) ); // false

** 非运算符 ! 的优先级在所有逻辑运算符里面最高,所以它总是在 &&|| 前执行。**

3.4 乘性操作符

3.4.1 乘法操作符

符号:*

let result = 34 * 56;

不过,乘法操作符在处理特殊值时也有一些特殊的行为。

  • 如果操作数都是数值,则执行常规的乘法运算,即两个正值相乘是正值,两个负值相乘也是正值,正负符号不同的值相乘得到负值。如果ECMAScript不能表示该乘积,则返回Infinity-Infinity
  • 如果有任一操作数是NaN,则返回NaN
  • 如果是Infinity乘以0,则返回NaN
  • 如果是Infinity乘以非0的有限数值,则根据第二个操作数的符号返回Infinity-Infinity
  • 如果是Infinity乘以Infinity,则返回Infinity
  • 如果有不是数值的操作数,则先在后台用Number()将其转换为数值,然后再应用上述规则。

3.4.2 除法操作符

除法操作符由一个斜杠(/)表示,用于计算第一个操作数除以第二个操作数的商,比如:

let result = 66 / 11;

跟乘法操作符一样,除法操作符针对特殊值也有一些特殊的行为。

  • 如果操作数都是数值,则执行常规的除法运算,即两个正值相除是正值,两个负值相除也是正值,符号不同的值相除得到负值。如果ECMAScript不能表示商,则返回Infinity-Infinity
  • 如果有任一操作数是NaN,则返回NaN
  • 如果是Infinity除以Infinity,则返回NaN
  • 如果是0除以0,则返回NaN
  • 如果是非0的有限值除以0,则根据第一个操作数的符号返回Infinity-Infinity
  • 如果是Infinity除以任何数值,则根据第二个操作数的符号返回Infinity-Infinity
  • 如果有不是数值的操作数,则先在后台用Number()函数将其转换为数值,然后再应用上述规则。

3.4.3 取模操作符

取模(余数)操作符由一个百分比符号(%)表示,比如:

let result = 26 % 5; // 等于1

与其他乘性操作符一样,取模操作符对特殊值也有一些特殊的行为。

  • 如果操作数是数值,则执行常规除法运算,返回余数。
  • 如果被除数是无限值,除数是有限值,则返回NaN
  • 如果被除数是有限值,除数是0,则返回NaN
  • 如果是Infinity除以Infinity,则返回NaN
  • 如果被除数是有限值,除数是无限值,则返回被除数。
  • 如果被除数是0,除数不是0,则返回0。
  • 如果有不是数值的操作数,则先在后台用Number()函数将其转换为数值,然后再应用上述规则。

3.5 指数操作符

ECMAScript 7新增了指数操作符,Math.pow()现在有了自己的操作符**,结果是一样的:

console.log(Math.pow(3, 2);    // 9
console.log(3 ** 2);           // 9

console.log(Math.pow(16, 0.5); // 4
console.log(16** 0.5);         // 4

不仅如此,指数操作符也有自己的指数赋值操作符**=,该操作符执行指数运算和结果的赋值操作:

let squared = 3;
squared **= 2;
console.log(squared); // 9

let sqrt = 16;
sqrt **= 0.5;
console.log(sqrt); // 4

3.6 加性操作符

加性操作符在后台会发生不同数据类型的转换。

3.6.1 加法操作符

加法操作符(+)用于求两个数的和,比如:

let result = 1 + 2;

如果两个操作数都是数值,加法操作符执行加法运算并根据如下规则返回结果:

  • 如果有任一操作数是NaN,则返回NaN
  • 如果是InfinityInfinity,则返回Infinity
  • 如果是-Infinity-Infinity,则返回-Infinity
  • 如果是Infinity-Infinity,则返回NaN
  • 如果是+0+0,则返回+0
  • 如果是-0+0,则返回+0
  • 如果是-0-0,则返回-0

不过,如果有一个操作数是字符串,则要应用如下规则:

  • 如果两个操作数都是字符串,则将第二个字符串拼接到第一个字符串后面;
  • 如果只有一个操作数是字符串,则将另一个操作数转换为字符串,再将两个字符串拼接在一起。
  • 对于另一操作数,如果是对象、数值或布尔值,则调用他们的toString()方法获取字符串。
  • 对于undefinednull调用String()转型函数返回undefinednull这两个字符串。

注意:

ECMAScript中最常犯的一个错误,就是忽略加法操作中涉及的数据类型。比如下面这个例子:

let num1 = 5;
let num2 = 10;
let message = "The sum of 5 and 10 is " + num1 + num2;
console.log(message);  // "The sum of 5 and 10 is 510"

这里,变量message中保存的是一个字符串,是执行两次加法操作之后的结果。有人可能会认为最终得到的字符串是"The sum of 5 and 10 is 15"。可是,实际上得到的是"The sum of 5 and 10 is 510"。这是因为每次加法运算都是独立完成的。第一次加法的操作数是一个字符串和一个数值(5),结果还是一个字符串。第二次加法仍然是用一个字符串去加一个数值(10),同样也会得到一个字符串。如果想真正执行数学计算,然后把结果追加到字符串末尾,只要使用一对括号即可:

let num1 = 5;
let num2 = 10;
let message = "The sum of 5 and 10 is " + (num1 + num2);
console.log(message); // "The sum of 5 and 10 is 15"复制代码

在此,我们用括号把两个数值变量括了起来,意思是让解释器先执行两个数值的加法,然后再把结果追加给字符串。因此,最终得到的字符串变成了"The sum of 5 and 10 is 15"

3.6.2 减法操作符

减法操作符(-)也是使用很频繁的一种操作符,比如:

let result = 2 - 1;

与加法操作符一样,减法操作符也有一组规则用于处理ECMAScript中不同类型之间的转换。

  • 如果两个操作数都是数值,则执行数学减法运算并返回结果。
  • 如果有任一操作数是NaN,则返回NaN
  • 如果是InfinityInfinity,则返回NaN
  • 如果是-Infinity-Infinity,则返回NaN
  • 如果是Infinity-Infinity,则返回Infinity
  • 如果是-InfinityInfinity,则返回-Infinity
  • 如果是+0+0,则返回+0
  • 如果是+0-0,则返回-0
  • 如果是-0-0,则返回+0
  • 如果有任一操作数是字符串、布尔值、nullundefined,则先在后台使用Number()将其转换为数值,然后再根据前面的规则执行数学运算。如果转换结果是NaN,则减法计算的结果是NaN
  • 如果有任一操作数是对象,则调用其valueOf()方法取得表示它的数值。如果该值是NaN,则减法计算的结果是NaN。如果对象没有valueOf()方法,则调用其toString()方法,然后再将得到的字符串转换为数值。

以下示例演示了上面的规则:

let result1 = 5 - true; // true被转换为1,所以结果是4
let result2 = NaN - 1;  // NaN
let result3 = 5 - 3;    // 2
let result4 = 5 - "";   // ""被转换为0,所以结果是5
let result5 = 5 - "2";  // "2"被转换为2,所以结果是3
let result6 = 5 - null; // null被转换为0,所以结果是5

3.7 关系操作符

关系操作符执行比较两个值的操作,包括小于(<)、大于(>)、小于等于(<=)和大于等于(>=),用法跟数学课上学的一样。这几个操作符都返回布尔值,如下所示:

let result1 = 5 > 3; // true
let result2 = 5 < 3; // false

与ECMAScript中的其他操作符一样,在将它们应用到不同数据类型时也会发生类型转换和其他行为。

  • 如果操作数都是数值,则执行数值比较。
  • 如果操作数都是字符串,则逐个比较字符串中对应字符的编码。
  • 如果有任一操作数是数值,则将另一个操作数转换为数值,执行数值比较。
  • 如果有任一操作数是对象,则调用其valueOf()方法,取得结果后再根据前面的规则执行比较。如果没有valueOf()操作符,则调用toString()方法,取得结果后再根据前面的规则执行比较。
  • 如果有任一操作数是布尔值,则将其转换为数值再执行比较。

注意:同类型的不会转换成number类型的,比如说'2''11' 这两个字符串类型做比较按照Unicode索引值应该是'2''11'

alert( '2' > 1 ); // true,字符串 '2' 会被转化为数字 2
alert( '01' == 1 ); // true,字符串 '01' 会被转化为数字 1

let result = "23" < "3"; // true 同类型不转换
  • 这里有一个规则,即任何关系操作符在涉及比较NaN时都返回false
let result1 = NaN < 3;  // false
let result2 = NaN >= 3; // false

在大多数比较的场景中,如果一个值不小于另一个值,那就一定大于或等于它。但在比较NaN时,无论是小于还是大于等于,比较的结果都会返回false

3.8 相等操作符

3.8.1 等于和不等于

等于的符号:==

不等于的符号 !=

  • 这两个操作符都会先进行类型转换(通常称为:强制类型转换)。
  • 转换完之后再确定操作数是否相等。

在转换操作数的类型时,遵循如下规则。

  • 如果任一操作数是布尔值,则将其转换为数值再比较是否相等。false转换为0,true转换为1。
  • 如果一个操作数是字符串,另一个操作数是数值,则尝试将字符串转换为数值,再比较是否相等。
  • 如果一个操作数是对象,另一个操作数不是,则调用对象的valueOf()方法取得其原始值,再根据前面的规则进行比较。

在进行比较时,会遵循如下规则

  • nullundefined相等。
  • nullundefined不能转换为其他类型的值再进行比较。
  • 如果有任一操作数是NaN,则相等操作符返回false,不相等操作符返回true。记住:即使两个操作数都是NaN,相等操作符也返回false,因为按照规则,NaN不等于NaN
  • 如果两个操作数都是对象,则比较它们是不是同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回true。否则,两者不相等。比较操作数所指向的对象地址。

下表总结了一些特殊情况及比较的结果。

表达式 结果
null == undefined true
"NaN" == NaN false
5 == NaN false
NaN == NaN false
NaN != NaN true
false == 0 true
true == 1 true
true == 2 false
undefined == 0 false
null == 0 false
"5" == 5 true

3.8.2 全等和不全等

与相等和不相等不同,全等和不全等不会进行转换。

全等符号:===

不全等符号:!==

let result1 = ("55" == 55);   // true,转换后相等
let result2 = ("55" === 55);  // false,不相等,因为数据类型不同

let result3 = ("55" != 55);  // false,转换后相等
let result4 = ("55" !== 55); // true,不相等,因为数据类型不同
  • 虽然null == undefinedtrue(因为这两个值类似),但null === undefinedfalse,因为它们不是相同的数据类型。

3.9 条件操作符(三元操作符)

就是三元运算符

通过三元运算符也可以实现if的操作

let company = prompt('Which company created JavaScript?', '');

(company == 'Netscape') ?
   alert('Right!') : alert('Wrong.');

3.10 赋值操作符

简单赋值用等于号(=)表示,将右手边的值赋给左手边的变量,如下所示:

let num = 10;

每个数学操作符以及其他一些操作符都有对应的复合赋值操作符:

  • 乘后赋值(*=
  • 除后赋值(/=
  • 取模后赋值(%=
  • 加后赋值(+=
  • 减后赋值(-=
  • 左移后赋值(<<=
  • 右移后赋值(>>=
  • 无符号右移后赋值(>>>=

这些操作符仅仅是简写语法,使用它们不会提升性能。

3.11 逗号操作符

逗号操作符可以用来在一条语句中执行多个操作,如下所示:

let num1 = 1, num2 = 2, num3 = 3;

在一条语句中同时声明多个变量是逗号操作符最常用的场景。不过,也可以使用逗号操作符来辅助赋值。在赋值时使用逗号操作符分隔值,最终会返回表达式中最后一个值:

let num = (5, 1, 4, 8, 0); // num的值为0

在这个例子中,num将被赋值为0,因为0是表达式中最后一项。逗号操作符的这种使用场景并不多见,但这种行为的确存在。

4. 语句

4.1 if语句

这是最常用的语句:

if (condition){
    statement1
} else {
  statement2
}

注意:这里的条件(condition)可以是任何表达式,并且求值结果不一定是布尔值。ECMAScript会自动调用Boolean()函数将这个表达式的值转换为布尔值。

还有一种else if的写法,不赘述了。

4.2 do-while 语句

特点:循环体里面的代码至少执行一次。(我不咋用,所有经常忘)

do {
  statement //循环体
} while (experssion);//experssion,求出表达式结果,然后调用Boolean()函数转换为布尔值

4.3 while 语句

特点:满足条件再进入循环

while(expression){
  
  statement;
  
}

4.4 for 语句

最常用的循环语句。能用while语句写的就一定能用for语句写。不解释了。

for (initialization; expression; post-loop-expression) {
  
  statement;
  
}

实例:

for(let i = 1; i < 10; i++){

	console.log(i);
	
}

使用for循环实现while循环

let i = 1;

for(,i < 10,){
  console.log(i);
  i++;
}

//等价于

while(i < 10){
  console.log(i);
}

4.5 for-in 语句

for-in语句是一种严格的迭代语句,用于枚举对象中的非符号键属性,语法如下:

for (property in expression) statement

下面是一个例子:

for (const propName in window) {
  document.write(propName);
}

这个例子使用for-in循环显示了BOM对象window的所有属性。每次执行循环,都会给变量propName赋予一个window对象的属性作为值,直到window的所有属性都被枚举一遍。与for循环一样,这里控制语句中的const也不是必需的。但为了确保这个局部变量不被修改,推荐使用const

ECMAScript中对象的属性是无序的,因此for-in语句不能保证返回对象属性的顺序。换句话说,所有可枚举的属性都会返回一次,但返回的顺序可能会因浏览器而异。

如果for-in循环要迭代的变量是nullundefined,则不执行循环体。

4.6 for-of语句

for-of语句是一种严格的迭代语句,用于遍历可迭代对象的元素,语法如下:

for (property of expression) statement复制代码

下面是示例:

for (const el of [2,4,6,8]) {
  document.write(el);
}复制代码

在这个例子中,我们使用for-of语句显示了一个包含4个元素的数组中的所有元素。循环会一直持续到将所有元素都迭代完。与for循环一样,这里控制语句中的const也不是必需的。但为了确保这个局部变量不被修改,推荐使用const

for-of循环会按照可迭代对象的next()方法产生值的顺序迭代元素。关于可迭代对象,本书将在第7章详细介绍。

如果尝试迭代的变量不支持迭代,则for-of语句会抛出错误。

注意 ES2018对for-of语句进行了扩展,增加了for-await-of循环,以支持生成期约(promise)的异步可迭代对象。相关内容将在附录A介绍。

4.7 标签语句

有时候我们需要从一次从多层嵌套的循环中跳出来。

例如,下述代码中我们的循环使用了 ij,从 (0,0)(3,3) 提示坐标 (i, j)

for (let i = 0; i < 3; i++) {

  for (let j = 0; j < 3; j++) {

    let input = prompt(`Value at coords (${i},${j})`, '');

    // 如果我想从这里退出并直接执行 alert('Done!')
  }
}

alert('Done!');

我们需要提供一种方法,以在用户取消输入时来停止这个过程。

input 之后的普通 break 只会打破内部循环。这还不够 —— 标签可以实现这一功能!

标签 是在循环之前带有冒号的标识符:

labelName: for (...) {
  ...
}

break <labelName> 语句跳出循环至标签处:

let num = 0;

outermost:
for (let i = 0; i < 10; i++) {
  for (let j = 0; j < 10; j++) {
    if (i == 5 && j == 5) {
      break outermost;
    }
    num++;
  }
}

console.log(num); // 55

在这个例子中,outermost标签标识的是第一个for语句。正常情况下,每个循环执行10次,意味着num++语句会执行100次,而循环结束时console.log的结果应该是100。但是,break语句带来了一个变数,即要退出到的标签。添加标签不仅让break退出(使用变量j的)内部循环,也会退出(使用变量i的)的外部循环。当执行到ij都等于5时,循环停止执行,此时num的值是55。continue语句也可以使用标签。

我们还可以将标签移至单独一行:

outer:
for (let i = 0; i < 3; i++) { ... }

continue 指令也可以与标签一起使用。在这种情况下,执行跳转到标记循环的下一次迭代。

4.8 with语句

看了看影响性能,且严格模式下用不了,所以不看了。

4.9 switch语句

语法:

switch (expression) {
  case value1:
    statement
    break;
  case value2:
    statement
    break;
  case value3:
    statement
    break;
  case value4:
    statement
    break;
  default:
    statement
}

一般来说我们必须要break,不然的话,switch语句会一直运行下去,直到遇到break和语句结束。

为避免不必要的条件判断,最好给每个条件后面都加上break语句。如果确实需要连续匹配几个条件,那么推荐写个注释表明是故意忽略了break,如下所示:

switch (i) {
  case 25:
    /*跳过*/
  case 35:
    console.log("25 or 35");
    break;
  case 45:
    console.log("45");
    break;
  default:
    console.log("Other");
}

虽然switch语句是从其他语言借鉴过来的,但ECMAScript为它赋予了一些独有的特性。首先,switch语句可以用于所有数据类型(在很多语言中,它只能用于数值),因此可以使用字符串甚至对象。其次,条件的值不需要是常量,也可以是变量或表达式。看下面的例子:

switch ("hello world")
  case "hello" + " world":
    console.log("Greeting was found.");
    break;
  case "goodbye":
    console.log("Closing was found.");
    break;
  default:
    console.log("Unexpected message was found.");
}

这个例子在switch语句中使用了字符串。第一个条件实际上使用的是表达式,求值为两个字符串拼接后的结果。因为拼接后的结果等于switch的参数,所以console.log会输出"Greeting was found."。能够在条件判断中使用表达式,就可以在判断中加入更多逻辑:

let num = 25;
switch (true) {
  case num < 0:
    console.log("Less than 0.");
    break;
  case num >= 0 && num <= 10:
    console.log("Between 0 and 10.");
    break;
  case num > 10 && num <= 20:
    console.log("Between 10 and 20.");
    break;
  default:
    console.log("More than 20.");//输出这个
}

上面的代码首先在外部定义了变量num,而传给switch语句的参数之所以是true,就是因为每个条件的表达式都会返回布尔值。条件的表达式分别被求值,直到有表达式返回true;否则,就会一直跳到default语句(这个例子正是如此)。

注意 switch语句在比较每个条件的值时会使用全等操作符,因此不会强制转换数据类型(比如,字符串"10"不等于数值10)。

5. 函数

函数对任何语言来说都是核心组件,因为它们可以封装语句,然后在任何地方、任何时间执行。ECMAScript中的函数使用function关键字声明,后跟一组参数,然后是函数体。

注意 第10章会更详细地介绍函数。

以下是函数的基本语法:

function functionName(arg0, arg1,...,argN) {
  statements
}

下面是一个例子:

function sayHi(name, message) {
  console.log("Hello " + name + ", " + message);
}

//调用

sayHi('meake', 'jiuzhe ?');
  • 函数可以不写return。
  • 不写return的函数默认返回undefined

严格模式对函数也有一些限制:

  • 函数不能以evalarguments作为名称;
  • 函数的参数不能叫evalarguments
  • 两个命名参数不能叫同一个名称。

如果违反上述规则,则会导致语法错误,代码也不会执行。

6. 小结

JavaScript的核心语言特性在ECMA-262中以伪语言ECMAScript的形式来定义。ECMAScript包含所有基本语法、操作符、数据类型和对象,能完成基本的计算任务,但没有提供获得输入和产生输出的机制。理解ECMAScript及其复杂的细节是完全理解浏览器中JavaScript的关键。下面总结一下ECMAScript中的基本元素。

  • ECMAScript中的基本数据类型包括UndefinedNullBooleanNumberStringSymbol
  • 与其他语言不同,ECMAScript不区分整数和浮点值,只有Number一种数值数据类型。
  • Object是一种复杂数据类型,它是这门语言中所有对象的基类。
  • 严格模式为这门语言中某些容易出错的部分施加了限制。
  • ECMAScript提供了C语言和类C语言中常见的很多基本操作符,包括数学操作符、布尔操作符、关系操作符、相等操作符和赋值操作符等。
  • 这门语言中的流控制语句大多是从其他语言中借鉴而来的,比如if语句、for语句和switch语句等。

ECMAScript中的函数与其他语言中的函数不一样。

  • 不需要指定函数的返回值,因为任何函数可以在任何时候返回任何值。
  • 不指定返回值的函数实际上会返回特殊值undefined
posted @ 2020-11-04 15:11  方阿森  阅读(115)  评论(0)    收藏  举报