任何语言的核心所描述的都是这门语言在最基本的层面上如何工作,涉及语法、操作符、数据类型以及内置功能,在此基础之上才可以构建复杂的解决方案
本章接下来的内容主要基于ECMAScript第6版。ES6
js的语法借鉴了c/c++,java。js是相对宽松的一门脚本语言
js,JS是俩个不同的声明
所谓标识符,就是变量、函数、属性或函数参数的名称
标识符由下划线,数字,字符组成。第一个字符不能是数字
不会被程序执行,是给程序员看的代码
// 单行注解
/ *
多行注解
* /
在js开头,或者某部分代码块中使用
"use strict";
let sum = a + b // 没有分号也有效,但不推荐
let diff = a - b; // 加分号有效,推荐
语句可以被写在代码块中,if单个代码建议也加上大括号(方便阅读)
{
let sum=a+b;
}
保留字简单来说就是未来的关键字
关键字与保留字是js内部的标识符有特定的功能,我们声明的标识符不能和其重名
ECMAScript变量是松散类型的,意思是变量可以用于保存任何类型的数据
有3个关键字可以声明变量:var、const和let。
此段代码声明了var类型变量名为message的变量,不给初始值的情况下js默认会给一个undefined
var message;
js中的赋值可以赋任意值包括数据类型,这就意味着数值类型可以随时改变
下述代码给message赋值hi并且为字符串类型,随后又赋值了100为数值类型
var message = "hi";
message=100; //合法,但不推荐
使用var操作符定义的变量会成为包含它的函数的局部变量,函数在运行完毕message会被销毁。并且只能在自己的作用域范围能够被使用
function test() {
var message = "hi"; // 局部变量
}
test();
console.log(message); // 出错!
在函数内定义变量时省略var操作符,可以创建一个全局变量:注意:必须在调用函数后使用
只要调用一次函数test(),就会定义这个变量,并且可以在函数外部访问到。
function test() {
message="hi"; //全局变量
}
test();
console.log(message); // "hi"
- 在严格模式下,如果像这样给未声明的变量赋值,则会导致抛出ReferenceError。
- 在局部作用域中定义的全局变量很难维护,也会造成困惑
定义多个变量
var message = "hi",
found = false,
age = 29;
使用var声明的变量会自动提升到函数作用域的顶部
此外,反复多次使用var声明同一个变量也没有问题:
function foo() {
console.log(age); // undefined
var age = 26;
}
foo(); // undefined
let声明的范围是块作用域,而var声明的范围是函数作用域。
if (true) {
var name = 'Matt';
console.log(name); // Matt
}
console.log(name); // Matt
if (true) {
let age = 26;
console.log(age); // 26
}
console.log(age); // ReferenceError: age没有定义
let不允许同一个块作用域中出现冗余声明,如果变量名存在作用域链遵循就近原则
let a=10;
let a=20; // erroe
let声明的变量不会在作用域中被提升。
// name会被提升
console.log(name); // undefined
var name = 'Matt';
// age不会被提升
console.log(age); // ReferenceError:age没有定义
let age = 26;
var name = 'Matt';
console.log(window.name); // 'Matt'
let age = 26;
console.log(window.age); // undefined
由于let的作用域是块级的可能会出现条件声明
let中根据不同的条件声明相同的变量这是没有必要的,代码也是不易理解的
if(a>10){
let b=300;
}else{
let b=20;
}
return b;
最好声明到外部,或者用三目运算符
let b
if(a>10){
b=300
}else{
b=20;
}
return b;
var由于函数作用域再var循环中使用会出现问题
for (var i = 0; i < 5; ++i) {
// 循环逻辑
}
console.log(i); // 5
let的作用域是块级的所以是不会出现这个问题
const声明的变量必须给初始值
const的行为与let基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,且尝试修改const声明的变量会导致运行时错误。
const a=10;
a=100; // error
const声明的限制只适用于它指向的变量的引用,换句话说如果是个对象,你改变对象里面的值则不会报错
const obj={};
obj.name="zhangsan";
ECMAScript有6种简单数据类型。Undefined、Null(本质上是Object)、Boolean、Number、String和Symbol
还有一种复杂数据类型Objec
只有7种数据类型似乎不足以表示全部数据。但ECMAScript的数据类型很灵活,一种数据类型可以当作多种数据类型来使用
typeof判断数据的数据类型
let message = "some string";
console.log(typeof message); // "string"
console.log(typeof(message)); // "string"
console.log(typeof 95); // "number"
Undefined类型只有一个值,就是特殊值undefined
当使用var或let声明了变量但没有初始化时,就相当于给变量赋予了undefined值:
let message;
var t
console.log(message == undefined); // true
console.log(t== undefined); // true
- 声明了没有赋值和没有声明是由本质区别的 虽然typeof是返回是undefined类型
- 声明了没有赋值不会报错,没有声明使用了会报错
Null类型同样只有一个值,即特殊值null
逻辑上讲,null值表示一个空对象指针,这也是给typeof传一个null会返回"object"的原因:
let car = null;
console.log(typeof car); // "object"
undefined值是由null值派生而来的
console.log(null == undefined); // true
有两个字面值:true和false。
十进制可以直接写出来
int a=10;
八进制开头必须是0,如果值超出8进制的范围会按十进制处理,忽略0
let octalNum1 = 070; // 八进制的56
let octalNum2 = 079; // 无效的八进制值,当成79 处理
let octalNum3 = 08; // 无效的八进制值,当成8 处理
十六进制开头必须是0x,十六进制数字中的字母大小写均可
let hexNum1 = 0xA; // 十六进制10
let hexNum2 = 0x1f; // 十六进制31
let a=10.1;
因为存储浮点值使用的内存空间是存储整数值的两倍,所以ECMAScript总是想方设法把值转换为整数
let floatNum1 = 1.; // 小数点后面没有数字,当成整数1 处理
let floatNum2 = 10.0; // 小数点后面是零,当成整数10 处理
对于非常大或非常小的数值,浮点值可以用科学记数法来表示
let floatNum = 3.125e7; // 等于31250000
let num=0.0000003 // 等于3e-7
浮点值的精确度最高可达17位小数,但在算术计算中远不如整数精确。
0.1+0.2=0.30000000000000004
ECMAScript并不支持表示这个世界上的所有数值
最小值通过Number.MIN_VALUE可以活动,最大值Number.MAX_VALUE
如果某个计算值超过这个数字会转换为无穷大(正负)无穷数值不能运算
表示不是一个数,在其他的语言中除0会报错
console.log(0/0); // NaN
console.log(-0/+0); // NaN
如果分子是非0值,分母是有符号0或无符号0,则会返回Infinity或-Infinity:
console.log(5/0); // Infinity
console.log(5/-0); // -Infinity
NaN与任何数值比较都等于自己
console.log(NaN == NaN); // false
isNaN来判断一个数是否为数值
把一个值传给isNaN()后,该函数会尝试把它转换为数值。某些非数值的值可以直接转换成数值
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
Number()的转换规则如下
let num1 = Number("Hello world! "); // NaN
let num2 = Number(""); // 0
let num3 = Number("000011"); // 11
let num4 = Number(true); // 1
parseInt()的转换规则如下
let num1 = parseInt("1234blue"); // 1234
let num2 = parseInt(""); // NaN
let num3 = parseInt("0xA"); // 10,解释为十六进制整数
let num4 = parseInt(22.5); // 22
let num5 = parseInt("70"); // 70,解释为十进制值
let num6 = parseInt("0xf"); // 15,解释为十六进制整数
parseFloat()的转换规则和parseInt()类似不过是带小数
第一个主要的区别是第一次出现的小数点是有效的,但第二次出现的小数点就无效了,此时字符串的剩余字符都会被忽略。"22.34.5"将转换成22.34。
parseFloat()函数的另一个不同之处在于,它始终忽略字符串开头的零,16进制直接返回0,因此不能指定底数
如果字符串表示整数则返回整数
let num1 = parseFloat("1234blue"); // 1234,按整数解析
let num2 = parseFloat("0xA"); // 0
let num3 = parseFloat("22.5"); // 22.5
let num4 = parseFloat("22.34.5"); // 22.34
let num5 = parseFloat("0908.5"); // 908.5
let num6 = parseFloat("3.125e7"); // 31250000
字符串可以使用双引号(")、单引号(')或反引号(`)标示
不过要注意的是,以某种引号作为字符串开头,必须仍然以该种引号作为字符串结尾。
let firstName = "John";
let lastName = 'Jacob';
let lastName = `Jingleheimerschmidt`
// \u03a3由于是字面量所以被当作一个字符处理
let text = "This is the letter sigma: \u03a3."; // length==28
当操作字符串时,JavaScript 中的字符串是不可变的,这意味着对字符串的操作会创建一个新的字符串赋值,原始字符串本身不会被修改。
toString()方法可见于数值、布尔值、对象和字符串值。null和undefined值没有toString()方法。
let age = 11;
let ageAsString = age.toString(); // 字符串"11"
let found = true;
let foundAsString = found.toString(); // 字符串"true"
如果你不确定一个值是不是null或undefined,可以使用String()转型函数
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"
模板字面量保留换行字符,可以跨行定义字符串:
let myMultiLineString = 'first line\nsecond line';
let myMultiLineTemplateLiteral = `first line
second line`;
console.log(myMultiLineString);
// first line
// second line"
console.log(myMultiLineTemplateLiteral);
// first line
// second line
console.log(myMultiLineString === myMultiLinetemplateLiteral); // true
由于模板字面量会保持反引号内部的空格,因此在使用时要格外注意。格式正确的模板字符串看起来可能会缩进不当:
// 这个模板字面量在换行符之后有25 个空格符
let myTemplateLiteral = `first line
second line`;
console.log(myTemplateLiteral.length); // 47
// 这个模板字面量以一个换行符开头
let secondTemplateLiteral = `
first line
second line`;
console.log(secondTemplateLiteral[0] === '\n'); // true
// 这个模板字面量没有意料之外的字符
let thirdTemplateLiteral = `first line
second line`;
console.log(thirdTemplateLiteral);
// first line
// second line
字符串插值通过在${}中使用一个JavaScript表达式实现:
插值可以用于运算
const num='${1+1}' // num==2
插值可以用于调用方法
function foo(){
return "World"
}
console.log(`Hello, ${ foo() }! `); // Hello, World!
插值可以用于字符拼接
const str="hello";
const stl='${str} world'; // hello world
Symbol(符号)是ECMAScript 6新增的数据类型。符号是原始值,且符号实例是唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。
符号需要使用Symbol()函数初始化。
let sym = Symbol();
console.log(typeof sym); // symbol
调用Symbol()函数时,也可以传入一个字符串参数作为对符号的描述
let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();
let fooSymbol = Symbol('foo');
let otherFooSymbol = Symbol('foo');
console.log(genericSymbol == otherGenericSymbol); // false
console.log(fooSymbol == otherFooSymbol); // false
符号没有字面量语法,这也是它们发挥作用的关键。按照规范,你只要创建Symbol()实例并将其用作对象的新属性,就可以保证它不会覆盖已有的对象属性,无论是符号属性还是字符串属性。
利用Symbol来作为对象的属性名,Symbol定义的属性无法通过Object.keys()中的方法和遍历获取,实现属性的私有
var gender = Symbol();
var person = {
name: 'kreme',
age: 12,
[gender]: 'male'
}
Object.keys(person) // ['name', 'age']
for (let p in person) {
console.log(p) // 分别会输出:'name' 和 'age'
}
Object.getOwnPropertyNames(person) // ['name', 'age']
我们可以通过新的api获取
var gender = Symbol();
var person = {
name: 'kreme',
age: 12,
[gender]: 'male'
}
// 使用Object的API
Object.getOwnPropertySymbols(person) // [[gender]]
// 使用新增的反射API
Reflect.ownKeys(person) // [[gender], 'age', 'title']
使用Symbol()来定义常量,利用Symbol()的唯一性来定义常量
const a = Symbol();
const b = Symbol();
const c = Symbol();
定义某个类的私有属性或方法
// 这个是a文件内定义的Login类:
const password = Symbol()
class Login {
constructor(username, password) {
this.username = username
this[password] = password
}
checkPassword(pwd) {
return this[password] === pwd
}
}
export default Login; // 对外暴露了login方法
// 这个是在b文件内调用a文件中的方法:
const login = new Login('admin', '123456')
login.checkPassword('123456') // true
login.password // 无法访问
login[password] // 无法访问
login["password"] // 无法访问
ECMAScript中的对象其实就是一组数据和功能的集合。对象通过new操作符后跟对象类型的名称来创建。
开发者可以通过创建Object类型的实例来创建自己的对象,然后再给对象添加属性和方法:
let o = new Object();
let obj=new Object // 不推荐
每个Object实例都有如下属性和方法。
只操作一个值的操作符叫一元操作符
前缀运算符遵循先变后算,后缀运算符遵循先算后变
int num=10;
++num,num++,--num,num--
递增递减操作符对于其他类型运算的规则如下
let obj={
valueOf(){
return -1;
}
}
console.log(obj++); // -1
console.log(obj); // 0
一元加法和减法主要可以用于类型转换
数值
int a=10;
int b=20;
+a // 10
-b // -20
非数值,遵循Number()的转换规律。负号一样
let s1 = "01";
let s2 = "1.1";
let s3 = "z";
let b = false;
let f = 1.1;
let o = {
valueOf() {
return -1;
}
};
s1 = +s1; // 值变成数值1
s2 = +s2; // 值变成数值1.1
s3 = +s3; // 值变成NaN
b = +b; // 值变成数值0
f = +f; // 不变,还是1.1
o = +o; // 值变成数值-1
ECMAScript中的所有数值都以IEEE 754 64位格式存储,但位操作并不直接应用到64位表示,而是先把值转换为32位整数,再进行位操作,之后再把结果转换为64位。所以我们只需要考虑32即可
有符号整数使用32位的前31位表示整数值。第32位表示数值的符号,如0表示正,1表示负。这一位称为符号位
正值以真正的二进制格式存储,即31位中的每一位都代表2的幂。第一位(称为第0位)表示20,第二位表示21,依此类推。如果一个位是空的,则以0填充
数值18的二进制格式为00000000000000000000000000010010,或更精简的10010。
负值以一种称为二补数(或补码)的二进制编码存储
那么,-18的二进制表示就是11111111111111111111111111101110。要注意的是,在处理有符号整数时,我们无法访问第31位。
特殊值NaN和Infinity在位操作中都会被当成0处理。如果将位操作符应用到非数值,那么首先会使用Number()函数将该值转换为数值
按位非运算得到的数值要比数值加减运算效率更高,因为这是直接在操作内存
let num1 = 25; // 二进制00000000000000000000000000011001
let num2 = ~num1; // 二进制11111111111111111111111111100110
console.log(num2); // -26
是将二进制对其比较,都是1返回,否则返回0。计算并返回最终结果
let result = 25 & 3;
console.log(result); // 1
是将二进制对其比较,只要有一个1就返回1,否则返回0。计算并返回最终结果
let result = 25 | 3;
console.log(result); // 27
是将二进制对其比较,只在一位上是1的时候返回1,否则返回0。计算并返回最终结果
let result = 25 ^ 3;
console.log(result); // 26
无论应用到的是什么数据类型。逻辑非操作符首先将操作数转换为布尔值,然后再对其取反。
console.log(! false); // true
console.log(! "blue"); // false
console.log(!0); // true
console.log(! NaN); // true
console.log(! ""); // true
console.log(!12345); // false
同时使用!!效果和Boolean()一样
console.log(! ! "blue"); // true
console.log(! !0); // false
console.log(! ! NaN); // false
console.log(! ! ""); // false
console.log(! !12345); // true
逻辑与遵循,都真则真,否则为假
如果有操作数不是布尔值,则逻辑与并不一定会返回布尔值,而是遵循如下规则。
逻辑与操作符是一种短路操作符,意思就是如果第一个操作数决定了结果,那么永远不会对第二个操作数求值。
// 这里因为第一个操作符为true,所以运算到了第二个运算符又因为没有定义所以报错
let found = true;
let result = (found && someUndeclaredVariable); // 这里会出错
console.log(result); // 不会执行这一行
// 这里因为第一个操作符为false,所以结果被确定不再运算第二个操作符
let found = false;
let result = (found && someUndeclaredVariable);
console.log(result); // false
逻辑或遵循,有一个真就为真,否则为假
如果有操作数不是布尔值,则逻辑或并不一定会返回布尔值,而是遵循如下规则。
逻辑或也有短路的特性
// 这里因为第一个操作符为true,所以结果被确定不再运算第二个操作符
let found = true;
let result = (found && someUndeclaredVariable);
console.log(result); // false
// 这里因为第一个操作符为false,所以运算到了第二个运算符又因为没有定义所以报错
let found = false;
let result = (found && someUndeclaredVariable); // 报错
console.log(result); // 不会执行
如果乘性操作符有不是数值的操作数,则该操作数会在后台被使用Number()转型函数转换为数值
乘法操作符遵循如下规则
除法操作符遵循如下规则
取模操作符遵循如下规则
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
加法操作符遵循以下规则
加法操作符遵循以下规则
关系操作符执行比较两个值的操作,包括小于(<)、大于(>)、小于等于(<=)和大于等于(>=),用法跟数学课上学的一样。这几个操作符都返回布尔值。
关系操作符符遵循以下规则
即任何关系操作符在涉及比较NaN时都返回false
这两个操作符都会先进行类型转换(通常称为强制类型转换)再确定操作数是否相等。
相等和不相等操作符遵循如下规则。
全等和不全等操作符不存在类型的强制转换,直接是值的比较
由于相等和不相等操作符存在类型转换问题,因此推荐使用全等和不全等操作符。这样有助于在代码中保持数据类型的完整性。
variable = boolean_expression ? true_value : false_value;
=,+=,-=,…
let num1 = 1, num2 = 2, num3 = 3;
()中的值会自动用Boolean()转换,根据结果值执行对应语句
if (i > 25) {
console.log("Greater than 25.");
} else if (i < 0) {
console.log("Less than 0.");
} else {
console.log("Between 0 and 25, inclusive.");
}
let i = 0;
while (i < 10) {
i += 2;
}
let count = 10;
for (let i = 0; i < count; i++) {
console.log(i);
}
for-in语句是一种严格的迭代语句,用于枚举对象中的属性
每次执行循环,都会给变量propName赋予一个window对象的属性作为值,直到window的所有属性都被枚举一遍。
ECMAScript中对象的属性是无序的,因此for-in语句不能保证返回对象属性的顺序。换句话说,所有可枚举的属性都会返回一次,但返回的顺序可能会因浏览器而定
如果for-in循环要迭代的变量是null或undefined,则不执行循环体。
for (const propName in window) {
document.write(propName);
}
for-of语句是一种严格的迭代语句,用于枚举对象中的属性值
与for-in用法类型
for (const el of [2,4,6,8]) {
document.write(el);
}
break跳出临近循环
countinue跳出本次循环
必须加break,否则下面代码会以此执行。可以用return替代
default每次匹配都会被执行一次
switch (expression) {
case value1:
statement
break;
case value2:
statement
break;
case value3:
statement
break;
case value4:
statement
break;
default:
statement
}
函数对任何语言来说都是核心组件,因为它们可以封装语句,然后在任何地方、任何时间执行。
function sayHi(name, message) {
console.log("Hello " + name + ", " + message);
}
sayHi("Nicholas", "how are you today? ");
有返回值的函数
只要碰到return语句,函数就会立即停止执行并退出。因此,return语句后面的代码不会被执行
function sum(num1, num2) {
return num1 + num2;
}
const result = sum(5, 10);