文章目录
前言
通过前面一段时间的学习,我对JavaScript这门语言又有了进一步的认识和了解。今天我就给大家总结一下这第三章的内容,JavaScript的语言基础
提示:以下是本篇文章正文内容,下面案例可供参考
一、语法
1.区分大小写
首先要知道的是ECMAScript语言中一切都区分大小写!
2.标识符
所谓标识符,就是变量、函数、属性或函数参数的名称。标识符可由一个或多个下列字符组成:
- 首字母不可以是数字,可以是字母、下划线(_)、美元符号($)
- 剩下的其他字符可以是字母、数字、下划线(_)、美元符号($)
- 标识符中的字母可以是扩展ASCII中的字母,也可以是Unicode的字母字符(不推荐使用!)
- 标识符的命名方式一般使用首字母小写,之后每个单词的首字母大写的方式:myName、firstStudent。
3.注释
// 我是单行注释
/*
我是多行注释
*/
4.严格模式
ECMAScript5增加了严格模式这一概念。严格模式是一种不同的JavaScript解析和执行模型,ECMAScript3的一些不规范写法在该种模式下会报错,对于不安全的活动将抛出错误。要在整个脚本中使用严格模式,可在脚本开头加:"use strict";
。
选择这种语法形式的目的是不破坏ECMAScript3的语法。
也可以单独指定一个函数在严格模式下执行:
function Say() {
"use strict";
// 函数体
}
5.语句
- 语句末尾应以分号结尾
- if类的控制语句必须有代码块,即:
if (test) {sonsole.log("你好")}
二、关键字和保留字
ECMA-262描述了一组保留的关键字,这些关键字有特殊用途,比如表示控制语句的开始和结束,或者执行特定的操作。按照规定,关键字和保留字补课用作标识符或属性名。ECMA-262第6版规定的所有关键字:
规范中也描述了一组未来的保留字,同样不能用作标识符或属性名。虽然保留字在语言中没有特定用途,但它们是保留给将来做关键字用的。
以下是ECMA-262第6版为将来保留的所有词汇:
三、变量
三个关键字可以声明变量:let、const、var
1.var关键字
var message;// 只声明不初始化,返回undefined
console.log(message);// undefined
var text = "message";
text = "gu";// 合法,但不推荐
1.var的作用域:
// 使用var在函数里面声明一个变量,此时是一个局部变量
function Say() {
var text = "H";
console.log(text);// H
}
Say();
console.log(text); //报错
// 在函数里面如果变量不加var,则此时变量会变成全局变量
function Say() {
text="哈哈哈";
console.log(text);
}
Say(); //哈哈哈
console.log(text); //哈哈哈
// 如果声明多个变量,利用逗号分隔每个变量
var text = "hi",
message = 20,
found = true;
在严格模式下,不能定义名为eval、arguments的变量,否则会造成语法错误
2.var的变量提升
使用var时,下面的代码不会报错。这是因为使用var关键字声明的变量会自动提升到函数作用域顶部
function Say() {
console.log(text);
var text = "我是谁";
}
Say(); //undefined
上面代码相当于
function Say() {
var text;
console.log(text);
text = "我是谁";
}
Say(); //undefined
以上的案例就说明了变量提升,也就是把所有变量声明都拉到函数作用域的顶部。另外,反复多次使用var声明同一个变量也没问题。
function Say() {
var age = 18;
var age = 19;
var age = 20;
console.log(age);
}
Say(); //20
2.let声明
let和var作用差不多,但是区别很大。
1.第一个区别就是:let声明的范围是块作用域,var声明的范围是函数作用域
//这是var例子
if(true) {
var name = "zhang";
console.log(name); //zhang
}
console.log(name); //zhang
//这是let例子
if(true) {
let age = 18;
console.log(age); //18
}
console.log(age); //报错,age未定义
以上案例说明:age变量的作用域仅限于该if语句块的内部。块作用域是函数作用域的子集,因此适用于var的作用域限制也适用于let。
2.第二个区别就是:let不允许同一个块作用域中出现冗余声明
//var这样声明没问题
var age;
var age;
//let声明这样声明会报错
let name;
let name;//报错
声明冗余不会因为混用let和var受影响。这两个关键字声明的并不是不同类型的变量,他们只是指出变量在相关作用域中如何存在。
var name;
let name;//报错:SyntaxError
let age;
var age;//报错:SyntaxError
嵌套使用相同的标识符不会报错,这是因为同一个块中没有重复声明。
//var声明
var name = "张三";
console.log(name); //张三
if(true){
var name = "李四";
console.log(name); //李四
}
//let声名
let age = 20;
console.log(age); //20
if(true){
var age = 25;
console.log(age); //25
}
3.第三个区别就是:let声明的变量不会在作用域中提升。
在let声明之前的执行瞬间被称为暂时性死区,在此阶段引用任何后面才声明的变量都会抛出ReferenceError错误
//var声明的变量会变量提升
console.log(text); //undefined
var text = "哈哈哈";
//let声明的变量不会有变量提升
console.log(message); //报错
let message = "呵呵呵";
4.第四个区别:使用let声明在全局作用域中声明的变量不会成为window对象的属性(var声明的变量则会)
var name = "张三";
console.log(window.name); //张三
let age = 15;
console.log(window.age); //undefined
不过,let声明仍然是在全局作用域中发生的,相应变量会在页面的声明周期内延续。为了避免SyntaxError,必须确保页面不会重复声明同一个变量。
5.第五个区别:for循环中的let和var区别
for(var i = 0; i < 8; i++) {
console.log(i);
}
console.log(i); //7
for(let i = 0; i < 8; i++) {
console.log(i);
}
console.log(i); //报错
for(var i = 1; i <= 5; i++) {
setTimeout(() => console.log(i),0);
}
//输出5,5,5,5,5
/*
出现这样的原因就是因为在退出循环时,迭代变量保存的是导致循环结束的值:5。在之后执行超时逻辑时,所有的i就是同一个变量值。
*/
for(let i = 1; i <= 5; i++) {
setTimeout(() => console.log(i),0);
}
//输出1,2,3,4,5
/*
出现这样的原因就是JavaScript引擎在后台会为每一个迭代循环声明一个新的迭代变量。
每个setTimeout引用的都是不同的变量实例,所以输出的值是循环执行过程中每个迭代变量的值。
*/
代码如下(示例):
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
3.const声明
const的行为和let基本相同。唯一一个重要的区别就是:const声明变量时必须初始化变量,且尝试修改const声明的变量会报错。
const age = 20;
age = 30; //修改常量,报错
const name = "张三";
const name = "李四";//报错,不允许重复声明!
//const声明的作用域也是块
const school = "清华大学"
if(true) {
const school = "北京大学";
}
console.log(school); //清华大学
如果const变量引用的是一个对象,则修改该对象内部属性值并不违反const限制。
const obj = {};
obj.name = "宫野先生"; //不会报错
如果在循环中,你只想用const声明一个不会被修改的for循环变量,是可以的。例如:
let i = 1;
for(const j = 5; i <= 3; i++){
console.log(j);
}
// 5,5,5
for(const key in (a:1,b:2)) {
console.log(key);
}
// a,b
for(const value of [1,2,3,4,5,6]) {
console.log(value);
}
// 1,2,3,4,5,6
4.声明风格
1.不使用var
有了let
和const
可以提高代码质量,因为变量有了明确的作用域、声明位置、不变的值
2.const优先,let次之
使用const
声明可以让浏览器运行时强制保持变量不变,也可以让静态代码分析工具提前发现不合法的赋值操作。
四、数据类型
ECMAScript
有6中简单数据类型(原始类型):Undefined、Null、Number、Boolean、String、Symbol
。Symbol
是ECMAScript 6
新增的一种数据类型。还有一种复杂数据类型叫作Object
,Object
是一种无序名值对的集合。
1.typeof操作符
对一个值使用typeof
操作符会返回下列字符串之一:
- “undefined”:表示值未定义
- “boolean”:表示值为布尔值
- “string”:表示值为字符串
- “number”:表示值为数值
- “object”:表示值为对象(而不是函数)或null
- “function”:表示值为函数
- “symbol”:表示值为符号
使用typeof null
返回的是object
,因为特殊值null被认为是一个对空对象的引用。
2.Undefined类型
Undefined
类型只有一个值,就是undefined
。当使用var
或let
声明了变量但没有初始化时,就相当于给变量赋予了undefined
值。
let age;
var name;
console.loe(我是+name+",今年"+age+"岁");
//输出我是undefined,今年undefined岁。
如果没有声明,那么这个变量的类型同样为undefined
。
let age;
// var name;
console.log(age);//undefined
console.log(name);//报错,未定义
console.log(typeof age); //undefined
console.log(typeof name);//undefined
3.Null类型
Null
类型同样只有一个值null
。逻辑上讲,null
表示一个空对象指针,这也是typeof null
返回object
的原因。
let car = null;
console.log(typeof car); //object
undefined
值是由null
值派生而来的,因此说:
console.log(null == undefined); //true
4.Boolean类型
Boolean
类型是ECMAScript
中使用最频繁的类型之一,有两个字面值:true
和false
。这两个布尔值不同于数值,true≠1
,false≠0
。
虽然布尔值有两个,但是所与其他的ECMAScript
类型的值都有相应布尔值的等价形式。可以利用Boolean()
转型函数将其它数据类型转为Boolean
类型:
let message = "my name";
let messageBoolean = Boolean(message);
console.log(typeof messageBoolean);//Boolean
console.log(messageBoolean); //true
下面总结了不同类型值到布尔值的转换:
数据类型 | 转换为true的值 | 转换为false的值 |
---|---|---|
Boolean | true | false |
String | 非空字符串 | ""空字符串 |
Boolean | true | false |
Number | 非零数值(包括无穷值) | 0、NaN |
Object | 任意对象 | null |
Undefined | N/A(不存在) | undefined |
5.Number类型
Number
类型使用IEEE 754
格式表示整数和浮点值(在某些语言中也叫双精度值),不同数值类型相应的也有不同的数字字面量格式。
1.浮点值
要定义浮点数,数值中必须有小数点,小数点后面至少有一个整数,小数点前面可以没有整数,但不推荐。
例如:
let num = 0.314;
let num1 = 1.32;
let num2 = .1; //不推荐
浮点数的精度最高可达17位小数,但是在算术计算中不如整数精确。例如0.1+0.2得到的不是0.3,而是0.30000000000000004。由于这种细微的差别,导致很难测试特定的浮点值。例如:
if(a+b)==0.3{// 这样会出问题
console.log("结果是0.3");
}
如果两个数值分别是0.15和0.15,或者分别是0.25,0.05,那就没问题,如果两个数值分别为0.1和0.2,就会测试失败。
之所以会出现这样的舍入错误,是因为使用了IEE 754
数值,这种错误并非ECMAScript
独有。其他使用相同格式的语言也有同样问题。
2.值的范围
由于内存的限制,ECMAScript
并不支持这个世界上所有的值数值。ECMAScript
可以表示的最小数保存在Number.MIN_VALUE
里面,这个值在多数浏览器里面是5e-324;可以表示的最大数值保存在Number.MAX_VALUE
里面,这个值在多数浏览器里面是1.7976931348623157e+308。
如果计算得到的数值超出了JavaScript可以表示的范围,那么任何无法表示的正数是Infinity
,任何无法表示的负数是-Infinity
。要确定一个数是不是有限大,可以使用isFinite()
表示。例如:
let num = Number.MAX_VALUE + Number.MAX_VALUE;
console.log(isFinite(num));//false
使用Number.NEGATIVE_INFINITY
和Number.POSITIVE_INFINITY
也可以获取正、负Infinity
,即Infinity
和-Infinity
。
3.NaNNaN
是一个特殊的数值,顾名思义,NaN
就是"Not a Number"
的缩写,用于表示本来要返回数值的操作失败了。下面几种情况就会出现NaN
:
console.log(0/0); //NaN
console.log(+0/-0); //NaN
如果分子是非0数值,则会返回正、负Infinity
console.loe(5/0); //Infinity
console.log(-5/0); //-Infinity
另外,NaN
并不等于包括NaN
在内的任何值。
console.log(NaN == NaN); //false
ECMAScript
提供了一个isNaN()
函数来判断某一数据类型是否是NaN
。
console.log(isNaN(NaN)); //true
console.log(isNaN(10)); //false
console.log(isNaN("10")); //false
console.log(isNaN("blue")); //true
console.log(isNaN(true)); //false true可以转换为数值1
4.数值转换
有3个函数可以将非数值类转换为数值,Number()、parseInt()、parseFloat()
。Number()
是转型函数,可以用于任何数据类型。parseInt()、parseFloat()
主要用于将字符串转换为数值。Number()
转换的例子:
Number(true); //1
Number(false); //0
Number(12); //12
Number(null); //0
Number(undefined);//NaN
Number("Hello World!");//NaN
Number("");//0
Number("0012");//12
parseInt()
转换的例子:parseInt()
更专注于字符串是否包含数值模式,字符串前面的空格会被忽略,从第一个非空格字符开始转换。若第一个字符不是数字字符、加号、减号,就会返回NaN
,意味着空字符串也会返回NaN
。如果第一个字符是数字、加号、减号,就会依次检索每个字符,直到字符串末尾,如果碰到非数值字符,则会将其忽略。parseInt()
也会识别不同的整数格式,如果字符串以"0X"开头,就会以十六进制转换数值。
下面几个实例,会让大家更加明白以上几句话的含义。
parseInt("1234blue");//1234
parseInt("");//NaN
parseInt("0xA");//10,解释为十六进制数
parseInt("22.5");//22
parseInt("70");//70,解释为整数
parseInt("123yellow");//123,yellow忽略
parseInt("017");//15,解释为八进制数
parseInt()也接收第二个参数,用于指定进制数
parseInt("0xAF",16);//以十六进制解释,175
//事实上,如果提供了十六进制参数,那么字符串前面的0x就可以去掉
parseInt("AF",16);//175
parseInt("AF");//NaN
通过第二个参数,可以极大扩展转换后获得的结果类型。例如:
parseInt("10",2);//2,按二进制解析
parseInt("10",8);//8,按八进制解析
parseInt("10",10);//10,按十进制解析
parseInt("10",16);//16,按十六进制解析
parseFloat()
转换的例子:
parseFloat()函数与parseInt()的工作方式差不多,都是从位置0开始检测每个字符。同样,它也是解析到字符串末尾或者解析到一个无效的数值字符为止。parseFloat()
始终忽略字符串开头的零
parseFloat("123blue");//1234,按整数解析
parseFloat("0xA");//0
parseFloat("22.6");//22.6
parseFloat(22.3.1);//22.3
parseFloat("0905.6");//905.6
parseFloat("3.125e7");//31250000
6.String类型
1.字符字面量
字面量 | 含义 |
---|---|
\n | 换行 |
\t | 制表符 |
\b | 退格 |
\r | 回车 |
\f | 换页 |
\ \ | 反斜杠(\) |
\ ’ | 单引号(’) |
\ “ | 双引号 ” |
\` | 反引号 ` |
\xnn | 以十六进制编码nn表示的字符 |
\unnnn | 以十六进制编码nnn表示的Unicode字符 |
2.字符串的特点ECMAScript
中的字符串是不可变的,意思是一旦创建,它们的值就不能变了。
3.转换为字符串
有两种方式可以把一个值转换为字符串,第一个方法就是toString()
方法。toString()
方法可用于数值、布尔值、对象、字符串值。null
和undefined
没有toString()方法
let age = 11;
let ageStr = age.toString(); //字符串"11"
let res = true;
let resString = resString.toString(); //字符串"true"
一般情况下,toString()
方法中不接收任何参数,但是在对数值调用这个方法时,toString()
可以接收一个底数参数,即以什么底数来输出数值的字符串表示。
let num = 10;
console.log(num.toString());//"10"
console.log(num.toString(2));//"1010"
console.log(num.toString(8));//"12"
console.log(num.toString(10));//"10"
console.log(num.toString(16));//"a"
第二个方法就是String()
转型函数,如果你不确定一个值是不是null
或undefined
,就可以使用String()
方法。该方法遵循以下规则:
- 如果值由
Sting()
方法,则调用该方法并返回结果 - 如果是
null
,就返回"null"
- 如果是
undefined
,就返回"undefined"
例如:
let value1 = 10;
let value2 = true;
let value3 = null;
let value4;
console.log(String(value1));//"10"
console.log(String(value2));//"true"
console.log(String(value3));//"null"
console.log(String(value4));//"undefined"
4.模板字面量ECMAScript6
新增了模板字面量定义字符的能力。与使用单引号和双引号不同,模板字面量保留换行符,可以跨行定义字符串。
例如:
let fir = "first\nsecond";
let sec = `first
second`;
console.log(fir);//first
//second
console.log(sec);//first
//second
console.log(fir == sec); //true
5.字符串插值
模板字面量最常用的是支持字符串插值,也就是可以在一个连续定义中插入一个或多个值。技术上讲,模板字面量不是字符串,而是一种特殊的JavaScript
语法表达式,只不过求值后得到的是字符串。
字符串插值通过在${}
中使用一个JavaScript
表达式实现,例如:
let value = 5;
let exp = 'second';
//以前,字符串插值是这样实现的
let interString = value + ' to the ' + exp + 'power is' + (value * value);
//现在可以用模板字面量这样实现
let interString1 = `${value} to the ${exp} power is ${value * value}`;
console.log(interSting);//5 to the second power is 25
console.log(interSting1);//5 to the second power is 25
所有插入的值都会使用toString()
强制转换为字符串,而且任何JavaScript
表达式都可以用于插值。嵌套的模板字符串无需转义:
console.log(`Hello,${`World`}`);//Hello,World
将表达式转换为字符串时会调用toString()
:
let foo = {toString:()=>'World'};
console.log(`Hello,${foo}!`);//Hello,World!
在插值表达式中可以调用函数和方法:
function cap(word) {
return `${word(0).toUpperCase()}${word.slice(1)}`;
}
console.log(`${cap('hello')},${cap(world)}!`);//Hello,World!
此外,模板也可以插入自己之前的值:
let value = '';
function append() {
value = `${value}abc`;
console.log(value);
}
append();//abc
append();//abcabc
append();//abcabcabc
6.模板字面量标签函数
模板字面量也支持定义标签函数,而通过标签函数可以自定义插值行为。标签函数会接收被插值记号分割后的模板和对每个表达式求值的结果。
标签函数本身是一个常规函数,通过前缀到模板字面量来应用自定义行为,如下例所示。标签函数接受到的参数依次是原始字符串数组和对每个表达式求值的结果。这个函数的返回值是对模板字面量求值得到的字符串。
let a = 6;
let b = 9;
function Tag(strings,aVal,bVal,sum) {
console.log(strings);
console.log(aVal);
console.log(bVal);
console.log(sum);
return 'foobar';
}
let res = `${a} + ${b} = ${a + b}`;
let tagRes = Tag`${a} + ${b} = ${a + b}`;
//[""," + ", " = ", ""]
//6
//9
//15
console.log(res);//"6 + 9 = 15"
console.log(tagRes);//foobar
因为表达式的参数数量是可以变的,所以通常应该使用剩余操作符将他们收集到一个数组中:
let a = 6;
let b = 9;
function Tag(strings,...expressions) {
console.log(strings);
for(const expression of expressions){
console.log(expression);
}
return 'foobar';
}
let tagRes = Tag`${a} + ${b} = ${a + b}`;
//[""," + ", " = ", ""]
//6
//9
//15
console.log(tagRes);//foobar
对于有n个插值的模板字面量,传给标签函数的表达式参数的个数始终是n,而传给标签函数的第一个参数所包含的字符串个数则始终是n+1。因此,如果你想把这些字符串和对表达式求值的结果拼接起来作为默认返回的字符串,可以这样做:
let a = 6;
let b = 9;
function Tag(strings,...expressions) {
return strings[0] + expressions.map((e,i) => `${e}${strings[i+1]}`).join('');
}
let unTagRes = `${a} + ${b} = ${a + b}`;
let tagRes = Tag`${a} + ${b} = ${a + b}`;
console.log(tagRes);//"6 + 9 = 15"
console.log(tagRes);//"6 + 9 = 15"
7.原始字符串
使用模板字符串也可以直接获取原始的模板字面量内容,而不是被转义后的字符表示。因此,可以使用默认的Sting.raw
函数
//Unicode示例
console.log(`\u00A9`);//©
console.log(String.row`\u00A9`);//\u00A9
//换行符示例
console.log(`first\nsecond`);//first
//second
console.log(String.raw`first\nsecond`);//first\nsecond
//对于实际的换行符是不行的
console.log(`first
second`);//first
//second
console.log(String.raw`first
second`);//first
//second
7.Symbol类型
Symbol
(符号)类型是ECMAScript6
新增的数据类型。符号是原始值,且符号实例是唯一的、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。
符号并不是为了提供私有属性增加的(尤其是因为Object API
提供了方法,可以更方便的发现符号属性)。相反,符号就是用来创建唯一记号,进而用作非字符串形式的对象属性。
1.符号的基本用法
符号需要使用Symbol()
函数初始化。
let sym = Symbol();
console.log(tyeof sym);//symbol
调用Symbol()函数时,也可以传入一个字符串作为对符号的描述,将来可以通过这个字符串来调试代码。但是,这个字符传参数与符号定义或标识完全无关。
例如:
let sym1 = Symbol();
let sym2 = Symbol();
let sym3 = Symbol('hello');
let sym4 = Symbol('hello');
console.log(sym1 == sym2);//false
console.log(sym3 == sym4);//false
符号没有字面量语法,按照规范,你只要创建Symbol()实例并将其样作对象的新属性,就可以保证它不会覆盖已有的对象属性,无论是符号属性还是字符串属性
let sym1 = Symbol();
consloe.log(sym1); //Symbol()
let sym2 = Symbol('hello');
console.log(sym2);//Symbol('hello');
Symbol()
不能用作构造函数,与new
关键字一起使用。这样做是为了避免创建符号包装对象,像使用Boolean、String、Number
那样,他们都支持构造函数且可用于初始化包含原始值的包装对象:
let myBoolean = new Boolean();
console.log(typeof myBoolean);//"object"
let myString = new String();
console.log(typeof myString);//"object"
let myNumber = new Number();
console.log(typeof myNumber);//"object"
let mySymbol = new Symbol();//TypeError: Symbol is not a constructor
//如果你确实想用符号包装对象,可以借用对象
let mySymbol1 = Symbol();
let myWrappendSymbol = Object(mySymbol);
console.log(typeof myWrappendSymbol);//"object"
2.使用全局符号注册表
如果运行时的不同部分需要共享和重用符号实例,那么可以用一个字符串作为键,在全局注册表中并重用符号。
为此,需要使用Symbol.for()
方法:
let GlobalSymbol = Symbol.for('foo');//创建新符号
let otherGlobalSymbol = Symbol.for('foo');//重用已有符号
console.log(GlobalSymbol = otherGlobalSymbol);//true
//即使采用相同的符号描述,在全局注册表中定义的符号跟使用Symbol()定义的符号也不等同
let symbol1 = Symbol('foo');
let symbol2 = Symbol.for('foo');
console.log(symbol1 === symbol2);//false
全局注册表中的符号必须使用字符串键来创建,因此作为参数传给Symbol.for()
的任何值都会被转换成字符串。此外,注册表中的键同时也会被用作符号描述。
let symbol1 = Symbol.for();
console.log(symbol1);//Symbol(undefined);
还可以使用Symbol.keyFor()
来查询全局注册表,这个方法接收符号,返回该全局符号对应的字符串键。如果查询的不是全局符号,则返回undefined
。
//创建全局符号
let symbol1 = Symbol.for('foo');
console.log(Symbol.keyFor(symbol1));//foo
//创建普通符号
let symbol2 = Symbol('gu');
console.log(Symbol.keyFor(symbol2));//undefined
//如果传给Symbol.keyFor()的不是符号,则会抛出TypeError错误
console.log(Symbol.keyFor(22));//TypeError:22 is not a symbol
3.使用符号作为属性
凡是可以使用字符串或数值作为属性的地方,都可以使用符号。这就包括了对象字面量属性和Object.defineProperty()/Object.defineProperties()
定义的属性。对象字面量只能在计算属性语法中使用符号作为属性。
let s1 = Symbol('foo'),
s2 = Symbol('bar'),
s3 = Symbol('baz'),
s4 = Symbol('qux');
let o = {
[s1]: 'foo val'
};
// 这样也可以:o[s1] = 'foo val';
console.log(o);
// {Symbol(foo): foo val}
Object.defineProperty(o, s2, {value: 'bar val'});
console.log(o);
// {Symbol(foo): foo val, Symbol(bar): bar val}
Object.defineProperties(o, {
[s3]: {value: 'baz val'},
[s4]: {value: 'qux val'}
});
console.log(o);
// {Symbol(foo): foo val, Symbol(bar): bar val,
// Symbol(baz): baz val, Symbol(qux): qux val}
类似于Object.getOwnPropertyNames()返回对象实例的常规属性数组,Object.getOwnProperty-Symbols()返回对象实例的符号属性数组。这两个方法的返回值彼此互斥。Object.getOwnProperty-Descriptors()会返回同时包含常规和符号属性描述符的对象。Reflect.ownKeys()会返回两种类型的键:
let s1 = Symbol('foo'),
s2 = Symbol('bar');
let o = {
[s1]: 'foo val',
[s2]: 'bar val',
baz: 'baz val',
qux: 'qux val'
};
console.log(Object.getOwnPropertySymbols(o));
// [Symbol(foo), Symbol(bar)]
console.log(Object.getOwnPropertyNames(o));
// ["baz", "qux"]
console.log(Object.getOwnPropertyDescriptors(o));
// {baz: {...}, qux: {...}, Symbol(foo): {...}, Symbol(bar): {...}}
console.log(Reflect.ownKeys(o));
// ["baz", "qux", Symbol(foo), Symbol(bar)]
因为符号属性是对内存中符号的一个引用,所以直接创建并用作属性的符号不会丢失。但是,如果没有显式地保存对这些属性的引用,那么必须遍历对象的所有符号属性才能找到相应的属性键:
let o = {
[Symbol('foo')]:'foo val',
[Symbol('bar')]:'bar val'
}
console.log(o);
// {Symbol(foo): "foo val", Symbol(bar): "bar val"}
let barSymbol = Object.getOwnPropertySymbols(o).find((symbol) => symbol.toString().match(/bar/));
console.log(barSymbol);
// Symbol(bar)
4.常用内置符号ECMAScript6
引入了一些常用内置符号,用于暴露语言内部行为,开发者可以直接访问、重写或模拟这些行为。这些内置符号都以Symbol
工厂函数字符串属性的形式存在。
这些内置符号最重要的用途之一就是重新定义他们,从而改变原生结构的行为。比如,我们知道for-of
循环会在相关对象上使用Symbol.iterator
属性,那么就可以通过在自定义对象上重新定义Symbol.iterator
的值,来改变for-of
在迭代该对象时的行为。
这些内置符号就是全局函数Symbol的普通字符串属性,指向一个符号的实例。所有内置符号属性的特点:不可写、不可枚举、不可配置。
5.Symbol.asyncIterator
根据ECMAScript6
规范,这个符号作为一个属性表示“一个方法,该方法返回对象默认的AsyncIterator。由for-await-if
语句使用”。换句话说,这个符号表示实现异步迭代器API的函数。
for-await-of循环会利用这个函数执行异步迭代操作。循环时,它们会调用以Symbol.asyncIterator为键的函数,并期望这个函数会返回一个实现迭代器API的对象。很多时候,返回的对象是实现该API的AsyncGenerator。
6.Symbol.hasInstance
根据ECMAScript
规范,这个符号作为一个属性表示“一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例。由instanceof
操作符使用”。instanceof
操作符可以用来确定一个对象实例的原型链上是否有原型。instanceof
的典型使用场景如下:
function Foo() {}
let f = new Foo();
console.log(f instanceof Foo); // true
class Bar {}
let b = new Bar();
console.log(b instanceof Bar); // true
在ES6
中,instanceof
操作符会使用Symbol.hasInstance
函数来确定关系。以Symbol. hasInstance
为键的函数会执行同样的操作,只是操作数对调了一下:
function Foo() {}
let f = new Foo();
console.log(Foo[Symbol.hasInstance](f)); // true
class Bar {}
let b = new Bar();
console.log(Bar[Symbol.hasInstance](b)); // true
这个属性定义在function
的原型上,因此默认在所有函数和类上都可以调用。由于instanceof
操作符会在原型链上寻找这个属性的定义,就跟在原型链上寻找其他属性一样,因此可以在继承的类上通过静态方法重新定义这个函数:
class Bar {}
class Baz extends Bar {
static [Symbol.hasInstance]() {
return false;
}
}
let b = new Baz();
console.log(Bar[Symbol.hasInstance](b)); // true
console.log(b instanceof Bar); // true
console.log(Baz[Symbol.hasInstance](b));//false
console.log(b instanceof Baz);//false
7.Symbol.isConcatSpreadable
根据ECMAScript
规范,这个符号作为一个属性表示“一个布尔值,如果是true
,则意味着对象应该用Array.prototype.concat()
打平其数组元素”。ES6
中的Array.prototype.concat()
方法会根据接收到的对象类型选择如何将一个类数组对象拼接成数组实例。覆盖Symbol.isConcat-Spreadable
的值可以修改这个行为。
数组对象默认情况下会被打平到已有的数组,false
或假值会导致整个对象被追加到数组末尾。类数组对象默认情况下会被追加到数组末尾,true
或真值会导致这个类数组对象被打平到数组实例。其他不是类数组对象的对象在Symbol.isConcatSpreadable
被设置为true
的情况下将被忽略。
let initial = ['foo'];
let array = ['bar'];
console.log(array[Symbol.isConcatSpreadable]);// undefined
console.log(initial.concat(array));// ['foo', 'bar']
array[Symbol.isConcatSpreadable] = false;
console.log(initial.concat(array));// ['foo', Array(1)]
let arrayLikeObject = { length: 1, 0: 'baz' };
console.log(arrayLikeObject[Symbol.isConcatSpreadable]);// undefined
console.log(initial.concat(arrayLikeObject));// ['foo', {...}]
arrayLikeObject[Symbol.isConcatSpreadable] = true;
console.log(initial.concat(arrayLikeObject));// ['foo', 'baz']
let otherObject = new Set().add('qux');
console.log(otherObject[Symbol.isConcatSpreadable]);//undefined
console.log(initial.concat(otherObject));// ['foo', Set(1)]
otherObject[Symbol.isConcatSpreadable] = true;
console.log(initial.concat(otherObject));// ['foo']
8.Symbol.iterator
根据ECMAScript
规范,这个符号作为一个属性表示“一个方法,该方法返回对象默认的迭代器。由for-of
语句使用”。换句话说,这个符号表示实现迭代器API
的函数。for-of
循环这样的语言结构会利用这个函数执行迭代操作。循环时,它们会调用以Symbol.iterator
为键的函数,并默认这个函数会返回一个实现迭代器API
的对象。很多时候,返回的对象是实现该API
的Generator
:
class Foo {
*[Symbol.iterator]() {}
}
let f = new Foo();
console.log(f[Symbol.iterator]());
// Generator {<suspended>}
9.Symbol.match
根据ECMAScript
规范,这个符号作为一个属性表示“一个正则表达式方法,该方法用正则表达式去匹配字符串。由String.prototype.match()
方法使用”。String.prototype.match()
方法会使用以Symbol.match
为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个String
方法的有效参数:
console.log(RegExp.prototype[Symbol.match]);
// f [Symbol.match]() { [native code] }
console.log('foobar'.match(/bar/));
// ["bar", index: 3, input: "foobar", groups: undefined]
给这个方法传入非正则表达式值会导致该值被转换为RegExp对象。如果想改变这种行为,让方法直接使用参数,则可以重新定义Symbol.match函数以取代默认对正则表达式求值的行为,从而让match()方法使用非正则表达式实例。Symbol.match函数接收一个参数,就是调用match()方法的字符串实例。返回的值没有限制:
class FooMatcher {
static [Symbol.match](target) {
return target.includes('foo');
}
}
console.log('foobar'.match(FooMatcher));//true
console.log('barbaz'.match(FooMatcher));//false
class StringMatcher() {
constructor(str){
this.str = str;
}
[Symbol.match](target) {
return target.includes(this.str);
}
}
console.log('foobar'.match(new StringMatcher('foo'))); //true
console.log('barbaz'.match(new StringMatcher('qux'))); //false
10.Symbol.replace
根据ECMAScript
规范,这个符号作为一个属性表示“一个正则表达式方法,该方法替换一个字符串中匹配的子串。由String.prototype.replace()
方法使用”。String.prototype.replace()
方法会使用以Symbol.replace
为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个String
方法的有效参数:
console.log(RegExp.prototype[Symbol.replace]);
// f [Symbol.replace]() { [native code] }
console.log('foobarbaz'.replace(/bar/, 'qux'));
// 'fooquxbaz'
给这个方法传入非正则表达式值会导致该值被转换为RegExp
对象。如果想改变这种行为,让方法直接使用参数,可以重新定义Symbol.replace
函数以取代默认对正则表达式求值的行为,从而让replace()
方法使用非正则表达式实例。Symbol.replace
函数接收两个参数,即调用replace()
方法的字符串实例和替换字符串。返回的值没有限制:
class FooReplacer {
static [Symbol.replace](target,replacement) {
return target.split('foo').join(replacement);
}
}
console.log('barfoobaz'.replace(FooReplacer),'qux');//"barquxbaz"
class StringReplacer() {
constructor(str){
this.str = str;
}
[Symbol.replace](target,replacement) {
return target.split('foo').join(replacement);
}
}
console.log('barfoobaz'.replace(new StringReplacer('foo'),'qux'));
//"barquxbaz"
11.Symbol.search
根据ECMAScript
规范,这个符号作为一个属性表示“一个正则表达式方法,该方法返回字符串中匹配正则表达式的索引。由String.prototype.search()
方法使用”。String.prototype.search()
方法会使用以Symbol.search
为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个String
方法的有效参数:
console.log(RegExp.prototype[Symbol.search]);
// f [Symbol.search]() { [native code] }
console.log('foobar'.search(/bar/));
// 3
给这个方法传入非正则表达式值会导致该值被转换为RegExp
对象。如果想改变这种行为,让方法直接使用参数,可以重新定义Symbol.search
函数以取代默认对正则表达式求值的行为,从而让search()
方法使用非正则表达式实例。Symbol.search
函数接收一个参数,就是调用match()
方法的字符串实例。返回的值没有限制:
class FooSearcher {
static [Symbol.search](target) {
return target.indexOf('foo');
}
}
console.log('foobar'.search(FooSearcher)); // 0
console.log('barfoo'.search(FooSearcher)); // 3
console.log('barbaz'.search(FooSearcher)); // -1
class StringSearcher {
constructor(str) {
this.str = str;
}
[Symbol.search](target) {
return target.indexOf(this.str);
}
}
console.log('foobar'.search(new StringSearcher('foo'))); // 0
console.log('barfoo'.search(new StringSearcher('foo'))); // 3
console.log('barbaz'.search(new StringSearcher('qux'))); // -1
12.Symbol.species
根据ECMAScript
规范,这个符号作为一个属性表示“一个函数值,该函数作为创建派生对象的构造函数”。这个属性在内置类型中最常用,用于对内置类型实例方法的返回值暴露实例化派生对象的方法。用Symbol.species
定义静态的获取器(getter)
方法,可以覆盖新创建实例的原型定义:
class Bar extends Array {}
class Baz extends Array {
static get [Symbol.species]() {
return Array;
}
}
let bar = new Bar();
console.log(bar instanceof Array); // true
console.log(bar instanceof Bar); // true
bar = bar.concat('bar');
console.log(bar instanceof Array); // true
console.log(bar instanceof Bar); // true
let baz = new Baz();
console.log(baz instanceof Array); // true
console.log(baz instanceof Baz); // true
baz = baz.concat('baz');
console.log(baz instanceof Array); // true
13.Symbol.split
根据ECMAScript
规范,这个符号作为一个属性表示“一个正则表达式方法,该方法在匹配正则表达式的索引位置拆分字符串。由String.prototype.split()
方法使用”。String.prototype. split()
方法会使用以Symbol.split
为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个String
方法的有效参数:
console.log(RegExp.prototype[Symbol.split]);
// f [Symbol.split]() { [native code] }
console.log('foobarbaz'.split(/bar/));
// ['foo', 'baz']
给这个方法传入非正则表达式值会导致该值被转换为RegExp
对象。如果想改变这种行为,让方法直接使用参数,可以重新定义Symbol.split
函数以取代默认对正则表达式求值的行为,从而让split()
方法使用非正则表达式实例。Symbol.split
函数接收一个参数,就是调用match()
方法的字符串实例。返回的值没有限制:
class FooSplitter {
static[Symbol.split](target) {
return target.split('foo');
}
}
console.log('barfoobaz'.split(FooSplitter));// ["bar", "baz"]
class StringSplitter {
constructor(str) {
this.str = str;
}
[Symbol.split](target){
return target.split(this.str);
}
}
console.log('barfoobaz'.split(new StringSplitter('foo')));
// ["bar", "baz"]
14.Symbol.toPrimitive
根据ECMAScript
规范,这个符号作为一个属性表示“一个方法,该方法将对象转换为相应的原始值。由ToPrimitive
抽象操作使用”。很多内置操作都会尝试强制将对象转换为原始值,包括字符串、数值和未指定的原始类型。对于一个自定义对象实例,通过在这个实例的Symbol.toPrimitive
属性上定义一个函数可以改变默认行为。
根据提供给这个函数的参数(string、number或default
),可以控制返回的原始值:
class Foo {}
let foo = new Foo();
console.log(3 + foo); // "3[object Object]"
console.log(3- foo); // NaN
console.log(String(foo)); // "[object Object]"
class Bar {
constructor() {
this[Symbol.toPrimitive] = function(hint) {
switch(hint){
case 'number':
return 3;
case 'string':
return 'string bar';
case 'default':
default:
return 'default bar';
}
}
}
}
let bar = new Bar();
console.log(3 + bar); // "3default bar"
console.log(3- bar); // 0
console.log(String(bar)); // "string bar"
15.Symbol.toStringTag
根据ECMAScript
规范,这个符号作为一个属性表示“一个字符串,该字符串用于创建对象的默认字符串描述。由内置方法Object.prototype.toString()
使用”。
通过toString()
方法获取对象标识时,会检索由Symbol.toStringTag
指定的实例标识符,默认为"Object"
。内置类型已经指定了这个值,但自定义类实例还需要明确定义:
let s = new Set();
console.log(s);// Set(0) {}
console.log(s.toString()); // [object Set]
console.log(s[Symbol.toStringTag]);// Set
class Foo {}
let foo = new Foo();
console.log(foo);// Foo {}
console.log(foo.toString());// [object Object]
console.log(foo[Symbol.toStringTag]);// undefined
class Bar {
constructor() {
this[Symbol.toStringTag] = 'Bar';
}
}
let bar = new Bar();
console.log(bar);// Bar {}
console.log(bar.toString());// [object Bar]
console.log(bar[Symbol.toStringTag]);// Bar
16.Symbol.unscopables
根据ECMAScript
规范,这个符号作为一个属性表示“一个对象,该对象所有的以及继承的属性,都会从关联对象的with
环境绑定中排除”。设置这个符号并让其映射对应属性的键值为true
,就可以阻止该属性出现在with
环境绑定中,如下例所示:
let o = { foo: 'bar' };
with (o) {
console.log(foo); // bar
}
o[Symbol.unscopables] = {
foo: true
};
with (o) {
console.log(foo); // ReferenceError
}
8.Object类型
ECMAScript
中的对象其实就是一组数据和功能的集合。对象通过new
操作符后跟对象类型的名称来创建。开发者可以通过创建Object
类型的实例来创建自己的对象,然后再给对象添加属性和方法:
let o = new Object();//正确创建对象的方法
let o1 = new Object;//合法,但不推荐
Object
类型的所有属性和方法在派生的对象上同样存在,每个Object
实例都有如下属性和方法。
- constructor:用于创建当前对象的函数。在前面的例子中,这个属性的值就是Object()函数。
- hasOwnProperty(propertyName):用于判断当前对象实例(不是原型)上是否存在给定的属性。要检查的属性名必须是字符串(如o.hasOwnProperty(“name”))或符号。
- isPrototypeOf(object):用于判断当前对象是否为另一个对象的原型。
- propertyIsEnumerable(propertyName):用于判断给定的属性是否可以使用for-in语句枚举。与hasOwnProperty()一样,属性名必须是字符串。
- toLocaleString():返回对象的字符串表示,该字符串反映对象所在的本地化执行环境。
- toString():返回对象的字符串表示。
- valueOf():返回对象对应的字符串、数值或布尔值表示。通常与toString()的返回值相同。
五、操作符
ECMA-262
描述了一组可用于操作数据值的操作符,包括数学操作符(如加、减)、位操作符、关系操作符和相等操作符等。ECMAScript
中的操作符是独特的,因为它们可用于各种值,包括字符串、数值、布尔值,甚至还有对象。在应用给对象时,操作符通常会调用valueOf()和/或toString()
方法来取得可以计算的值。
1.一元操作符
- 递增/递减操作符(++、–)
- 一元加减操作符(+、-)
2.位操作符
1.按位非
按位非操作符用波浪符(~)表示,它的作用是返回数值的一补数。按位非是ECMAScript
中为数不多的几个二进制数学操作符之一。下面的例子:
let num1 = 25;// 二进制00000000000000000000000000011001
let num2 = ~num1;// 二进制11111111111111111111111111100110
console.log(num2);// -26
这里,按位非操作符作用到了数值25,得到的结果是-26。由此可以看出,按位非的最终效果是对数值取反并减1,就像执行如下操作的结果一样:
let num1 = 25;
let num2 = -num1-1;
console.log(num2); // "-26"
2.按位与
按位与操作符用和号(&)表示,有两个操作数。本质上,按位与就是将两个数的每一个位对齐,然后基于真值表中的规则,对每一位执行相应的与操作。
按位与操作在两个位都是1时返回1,在任何一位是0时返回0。
let result = 25 & 3;
console.log(result); // 1
//解释
25 = 00000000000000000000000000011001
3 = 00000000000000000000000000000011
AND = 00000000000000000000000000000001
3.按位或
按位或操作符用管道符(|)表示,同样有两个操作数。按位或操作在至少一位是1时返回1,两位都是0时返回0。
let result = 25 | 3;
console.log(result); // 27
//解释
25 = 00000000000000000000000000011001
3 = 00000000000000000000000000000011
OR = 00000000000000000000000000011011
4.按位异或
按位异或用脱字符(^)表示,同样有两个操作数。按位异或与按位或的区别是,它只在一位上是1的时候返回1(两位都是1或0,则返回0)。
let result = 25 ^ 3;
console.log(result); // 26
//解释
25 = 00000000000000000000000000011001
3 = 00000000000000000000000000000011
XOR = 00000000000000000000000000011010
5.左移
左移操作符用两个小于号(<<)表示,会按照指定的位数将数值的所有位向左移动。比如,如果数值2(二进制10)向左移5位,就会得到64(二进制1000000),如下所示:
let oldValue = 2;// 等于二进制10
let newValue = oldValue << 5;// 等于二进制1000000,即十进制64
6.有符号右移
有符号右移由两个大于号(>>)表示,会将数值的所有32位都向右移,同时保留符号(正或负)。有符号右移实际上是左移的逆运算。比如,如果将64右移5位,那就是2:
let oldValue = 64;// 等于二进制1000000
let newValue = oldValue >> 5; // 等于二进制10,即十进制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位后,结果是134217726。这是因为-64的二进制表示是11111111111111111111111111000000,无符号右移却将它当成正值,也就是4294967232。把这个值右移5位后,结果是00000111111111111111111111111110,即134217726。
3.布尔操作符
1.逻辑非
逻辑非操作符由一个叹号(!)表示,可应用给ECMAScript中的任何值。这个操作符始终返回布尔值,无论应用到的是什么数据类型。逻辑非操作符首先将操作数转换为布尔值,然后再对其取反。换句话说,逻辑非操作符会遵循如下规则。
- 如果操作数是对象,则返回false。
- 如果操作数是空字符串,则返回true。
- 如果操作数是非空字符串,则返回false。
- 如果操作数是非0数值(包括Infinity),则返回false。
- 如果操作数是null,则返回true。
- 如果操作数是NaN,则返回true。
- 如果操作数是undefined,则返回true。
- 如果操作数是数值0,则返回true。
例如:
console.log(! false); // true
console.log(! "blue"); // false
console.log(!0); // true
console.log(! NaN); // true
console.log(! ""); // true
console.log(!12345); // false
2.逻辑与
逻辑与操作符由两个和号(&&)表示,应用到两个值,如下所示:
let result = true && false;
逻辑与操作符可用于任何类型的操作数,不限于布尔值。如果有操作数不是布尔值,则逻辑与并不一定会返回布尔值,而是遵循如下规则。
- 如果第一个操作数是对象,则返回第二个操作数。
- 如果第二个操作数是对象,则只有第一个操作数求值为true才会返回该对象。
- 如果两个操作数都是对象,则返回第二个操作数。
- 如果有一个操作数是null,则返回null。
- 如果有一个操作数是NaN,则返回NaN。
- 如果有一个操作数是undefined,则返回undefined。
逻辑与操作符是一种短路操作符,意思就是如果第一个操作数决定了结果,那么永远不会对第二个操作数求值。对逻辑与操作符来说,如果第一个操作数是false,那么无论第二个操作数是什么值,结果也不可能等于true。看下面的例子:
let found = true;
let result = (found && someUndeclaredVariable); // 这里会出错
console.log(result); // 不会执行这一行
3.逻辑或
逻辑或操作符由两个管道符(||)表示,比如:
let result = true || false;
与逻辑与类似,如果有一个操作数不是布尔值,那么逻辑或操作符也不一定返回布尔值。它遵循如下规则。
- 如果第一个操作数是对象,则返回第一个操作数。
- 如果第一个操作数求值为false,则返回第二个操作数。
- 如果两个操作数都是对象,则返回第一个操作数。
- 如果两个操作数都是null,则返回null。
- 如果两个操作数都是NaN,则返回NaN。
- 如果两个操作数都是undefined,则返回undefined。
同样与逻辑与类似,逻辑或操作符也具有短路的特性。只不过对逻辑或而言,第一个操作数求值为true,第二个操作数就不会再被求值了。看下面的例子:
let found = true;
let result = (found || someUndeclaredVariable); // 不会出错
console.log(result); // 会执行
4.乘性操作符
1.乘法操作符(*)
乘法操作符由一个星号(*)表示,可以用于计算两个数值的乘积。
let res = 9 * 9;//81
乘法操作符在处理特殊值时也有一些特殊的行为。
- 如果操作数都是数值,则执行常规的乘法运算,即两个正值相乘是正值,两个负值相乘也是正值,正负符号不同的值相乘得到负值。如果ECMAScript不能表示乘积,则返回Infinity或-Infinity。
- 如果有任一操作数是NaN,则返回NaN。
- 如果是Infinity乘以0,则返回NaN。
- 如果是Infinity乘以非0的有限数值,则根据第二个操作数的符号返回Infinity或-Infinity。
- 如果是Infinity乘以Infinity,则返回Infinity。
- 如果有不是数值的操作数,则先在后台用Number()将其转换为数值,然后再应用上述规则。
2.除法操作符(/)
除法操作符由一个斜杠(/)表示,用于计算第一个操作数除以第二个操作数的商。
let res = 9 / 3;//3
跟乘法操作符一样,除法操作符针对特殊值也有一些特殊的行为。
- 如果操作数都是数值,则执行常规的除法运算,即两个正值相除是正值,两个负值相除也是正值,符号不同的值相除得到负值。如果ECMAScript不能表示商,则返回Infinity或-Infinity。
- 如果有任一操作数是NaN,则返回NaN。
- 如果是Infinity除以Infinity,则返回NaN。
- 如果是0除以0,则返回NaN。
- 如果是非0的有限值除以0,则根据第一个操作数的符号返回Infinity或-Infinity。
- 如果是Infinity除以任何数值,则根据第二个操作数的符号返回Infinity或-Infinity。
- 如果有不是数值的操作数,则先在后台用Number()函数将其转换为数值,然后再应用上述规则。
3.取模操作符(%)
取模(余数)操作符由一个百分比符号(%)表示,比如:
let res = 34 % 10;//4
与其他乘性操作符一样,取模操作符对特殊值也有一些特殊的行为。
- 如果操作数是数值,则执行常规除法运算,返回余数。
- 如果被除数是无限值,除数是有限值,则返回NaN。
- 如果被除数是有限值,除数是0,则返回NaN。
- 如果是Infinity除以Infinity,则返回NaN。
- 如果被除数是有限值,除数是无限值,则返回被除数。
- 如果被除数是0,除数不是0,则返回0。
- 如果有不是数值的操作数,则先在后台用Number()函数将其转换为数值,然后再应用上述规则。
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
6.加性操作符
加性操作符,即加法和减法操作符,一般都是编程语言中最简单的操作符。不过,在ECMAScript
中,这两个操作符拥有一些特殊的行为。与乘性操作符类似,加性操作符在后台会发生不同数据类型的转换。只不过对这两个操作符来说,转换规则不是那么直观。
1.加法操作符
加法操作符(+)用于求两个数的和,比如:
let res = 1 + 2;
如果两个操作数都是数值,加法操作符执行加法运算并根据如下规则返回结果:
- 如果有任一操作数是NaN,则返回NaN;
- 如果是Infinity加Infinity,则返回Infinity;
- 如果是-Infinity加-Infinity,则返回-Infinity;
- 如果是Infinity加-Infinity,则返回NaN;
- 如果是+0加+0,则返回+0;
- 如果是-0加+0,则返回+0;
- 如果是-0加-0,则返回-0。
不过,如果有一个操作数是字符串,则要应用如下规则:
- 如果两个操作数都是字符串,则将第二个字符串拼接到第一个字符串后面;
- 如果只有一个操作数是字符串,则将另一个操作数转换为字符串,再将两个字符串拼接在一起。
let result1 = 5 + 5; // 两个数值
console.log(result1); // 10
let result2 = 5 + "5"; // 一个数值和一个字符串
console.log(result2); // "55"
2.减法操作符(-)
减法操作符(-)也是使用很频繁的一种操作符,比如:
let result = 2-1;
与加法操作符一样,减法操作符也有一组规则用于处理ECMAScript中不同类型之间的转换。
- 如果两个操作数都是数值,则执行数学减法运算并返回结果。
- 如果有任一操作数是NaN,则返回NaN。
- 如果是Infinity减Infinity,则返回NaN。
- 如果是-Infinity减-Infinity,则返回NaN。
- 如果是-Infinity减Infinity,则返回-Infinity。
- 如果是Infinity减-Infinity,则返回Infinity。
- 如果是+0减+0,则返回+0。
- 如果是+0减-0,则返回-0。
- 如果是-0减-0,则返回+0。
- 如果有任一操作数是字符串、布尔值、null或undefined,则先在后台使用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
7.关系运算符
关系操作符执行比较两个值的操作,包括小于(<)、大于(>)、小于等于(<=)和大于等于(>=)。与ECMAScript中的其他操作符一样,在将它们应用到不同数据类型时也会发生类型转换和其他行为。
- 如果操作数都是数值,则执行数值比较。
- 如果操作数都是字符串,则逐个比较字符串中对应字符的编码。
- 如果有任一操作数是数值,则将另一个操作数转换为数值,执行数值比较。
- 如果有任一操作数是对象,则调用其valueOf()方法,取得结果后再根据前面的规则执行比较。如果没有valueOf()操作符,则调用toString()方法,取得结果后再根据前面的规则执行比较。
- 如果有任一操作数是布尔值,则将其转换为数值再执行比较。
1.等于和不等于ECMAScript
中的等于操作符用两个等于号(==)表示,如果操作数相等,则会返回true
。不等于操作符用叹号和等于号(! =)表示,如果两个操作数不相等,则会返回true
。这两个操作符都会先进行类型转换(通常称为强制类型转换)再确定操作数是否相等。
在转换操作数的类型时,相等和不相等操作符遵循如下规则。
- 如果任一操作数是布尔值,则将其转换为数值再比较是否相等。false转换为0, true转换为1。
- 如果一个操作数是字符串,另一个操作数是数值,则尝试将字符串转换为数值,再比较是否相等。
- 如果一个操作数是对象,另一个操作数不是,则调用对象的valueOf()方法取得其原始值,再根据前面的规则进行比较。
- null和undefined相等。
- null和undefined不能转换为其他类型的值再进行比较。
- 如果有任一操作数是NaN,则相等操作符返回false,不相等操作符返回true。记住:即使两个操作数都是NaN,相等操作符也返回false,因为按照规则,NaN不等于NaN。
- 如果两个操作数都是对象,则比较它们是不是同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回true。否则,两者不相等。
2.全等和不全等
全等和不全等操作符与相等和不相等操作符类似,只不过它们在比较相等时不转换操作数。全等操作符由3个等于号(===)表示,只有两个操作数在不转换的前提下相等才返回true,比如:
let result1 = ("55" == 55); // true,转换后相等
let result2 = ("55" === 55); // false,不相等,因为数据类型不同
//null ===undefined是false,因为它们不是相同的数据类型。
注意 由于相等和不相等操作符存在类型转换问题,因此推荐使用全等和不全等操作符。这样有助于在代码中保持数据类型的完整
8.条件操作符
条件操作符是ECMAScript中用途最为广泛的操作符之一,语法跟Java中一样:
variable = boolean_expression ? true_value : false_value;
9.赋值运算符
每个数学操作符以及其他一些操作符都有对应的复合赋值操作符:
- 乘后赋值(*=)
- 除后赋值(/=)
- 取模后赋值(%=)
- 加后赋值(+=)
- 减后赋值(-=)
- 左移后赋值(<<=)
- 右移后赋值(>>=)
- 无符号右移后赋值(>>>=)
10.逗号操作符
逗号操作符可以用来在一条语句中执行多个操作,如下所示:
let num1 = 1, num2 = 2, num3 = 3;
let num = (1,2,3,4,5);//num值为5
六、语句
ECMA-262
描述了一些语句(也称为流控制语句),而ECMAScript中的大部分语法都体现在语句中。语句通常使用一或多个关键字完成既定的任务。
1.if语句
if
语句是使用最频繁的语句之一,语法如下:
if (condition) statement1 else statement2
这里的条件(condition)
可以是任何表达式,并且求值结果不一定是布尔值。ECMAScript
会自动调用Boolean()
函数将这个表达式的值转换为布尔值。
2.do-while语句
do-while
语句是一种后测试循环语句,即循环体中的代码执行后才会对退出条件进行求值。换句话说,循环体内的代码至少执行一次。do-while
的语法如下:
do {
statement
} while (expression);
3.while语句
while
语句是一种先测试循环语句,即先检测退出条件,再执行循环体内的代码。因此,while
循环体内的代码有可能不会执行。下面是while
循环的语法:
while(expression) statement
4.for语句
for
语句也是先测试语句,只不过增加了进入循环之前的初始化代码,以及循环执行后要执行的表达式,语法如下:
for (initialization; expression; post-loop-expression) statement
4.for-in语句
for-in
语句是一种严格的迭代语句,用于枚举对象中的非符号键属性,语法如下:
for (property in expression) statement
下面是一个例子:
for (const propName in window) {
document.write(propName);
}
这个例子使用for-in
循环显示了BOM
对象window的所有属性。如果for-in
循环要迭代的变量是null
或undefined
,则不执行循环体。
5.for-of语句
for-of
语句是一种严格的迭代语句,用于遍历可迭代对象的元素,语法如下:
for (property of expression) statement
举例:
for (const el of [2,4,6,8]) {
document.write(el);
}
6.break和continue语句
break
和continue
语句为执行循环代码提供了更严格的控制手段。其中,break
语句用于立即退出循环,强制执行循环后的下一条语句。而continue
语句也用于立即退出循环,但会再次从循环顶部开始执行。下面看一个例子:
let num = 0;
for (let i = 1; i < 10; i++) {
if (i % 5 == 0) {
break;
}
num++;
}
console.log(num); // 4
let num = 0;
for (let i = 1; i < 10; i++) {
if (i % 5 == 0) {
continue;
}
num++;
}
console.log(num); // 8
break
和continue
都可以与标签语句一起使用,返回代码中特定的位置。这通常是在嵌套循环中,如下面的例子所示:
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
let num = 0;
outermost:
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
if (i == 5 && j == 5) {
continue outermost;
}
num++;
}
}
console.log(num); // 95
7.with语句
with语句的用途是将代码作用域设置为特定的对象,其语法是:
with (expression) statement;
使用with语句的主要场景是针对一个对象反复操作,这时候将代码作用域设置为该对象能提供便利,如下面的例子所示:
let qs = location.search.substring(1);
let hostName = location.hostname;
let url = location.href;
8.switch语句
switch
语句是与if语句紧密相关的一种流控制语句,从其他语言借鉴而来。ECMAScript
中switch
语句跟C语言中switch
语句的语法非常相似,如下所示:
switch (expression) {
case value1:
statement
break;
case value2:
statement
break;
case value3:
statement
break;
case value4:
statement
break;
default:
statement
}
示例:
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.");
}
注意: switch
语句在比较每个条件的值时会使用全等操作符,因此不会强制转换数据类型(比如,字符串"10"不等于数值10)。
七、函数
函数对任何语言来说都是核心组件,因为它们可以封装语句,然后在任何地方、任何时间执行。ECMAScript
中的函数使用function
关键字声明,后跟一组参数,然后是函数体。
以下是函数的基本语法:
function functionName(arg0, arg1, ..., argN) {
statements
}
示例:
function sayHi(name, message) {
console.log("Hello " + name + ", " + message);
}
可以通过函数名来调用函数,要传给函数的参数放在括号里(如果有多个参数,则用逗号隔开)。下面是调用函数sayHi()
的示例:
sayHi("张三","28");//Hello,张三,28
ECMAScript
中的函数不需要指定是否返回值。任何函数在任何时间都可以使用return
语句来返回函数的值,用法是后跟要返回的值。比如:
function sum(num1, num2) {
return num1 + num2;
}
函数sum()
会将两个值相加并返回结果。注意,除了return
语句之外没有任何特殊声明表明该函数有返回值。然后就可以这样调用它:
const result = sum(5, 10);
要注意的是,只要碰到return
语句,函数就会立即停止执行并退出。因此,return
语句后面的代码不会被执行。比如:
function sum(num1, num2) {
return num1 + num2;
console.log("哈哈哈");//不会执行
}
注意 最佳实践是函数要么返回值,要么不返回值。只在某个条件下返回值的函数会带来麻烦,尤其是调试时。
严格模式对函数也有一些限制:
- 函数不能以
eval
或arguments
作为名称; - 函数的参数不能叫
eval
或arguments
; - 两个命名参数不能拥有同一个名称。
总结
以上就是今天我总结的近期学习的第三章内容,希望对大家的学习有所帮助,也希望大家对我的文章进行批评指正。