基础知识
1.外部脚本
如果你有大量的 JavaScript 代码,我们可以将它放入一个单独的文件。
脚本文件可以通过src
特性添加到 HTML 文件中。
<script src="/path/to/script.js"></script>
这里,/path/to/script.js
是脚本文件从网站根目录开始的绝对路径。当然也可以提供当前页面的相对路径。例如,src ="script.js"
表示当前文件夹中的"script.js"
文件。或者:
<script src="/ajax/libs/lodash.js/4.17.11/lodash.js"></script>
一般来说,只有最简单的脚本才嵌入到 HTML 中。更复杂的脚本存放在单独的文件中。
使用独立文件的好处是浏览器会下载它,然后将它保存到浏览器的缓存中。
之后,其他页面想要相同的脚本就会从缓存中获取,而不是下载它。所以文件实际上只会下载一次。
这可以节省流量,并使得页面(加载)更快。
如果设置了 src 特性,script 标签内容将会被忽略。
2.代码格式
语句
在语句后面不加分号大部分时候不影响运行,但是建议加上。
在语句之后,JavaScript 将分行符理解成“隐式”的分号。这也被称为自动分号插入
alert('Hello');alert('World');
注释
有两种形式,分为单行注释和多行注释
// 这行注释独占一行alert('Hello');alert('World'); // 这行注释跟随在语句后面
/* 两个消息的例子。这是一个多行注释。*/alert('Hello');alert('World');
3.现代模式——严格模式
编辑器中使用
长久以来,JavaScript 不断向前发展且并未带来任何兼容性问题。新的特性被加入,旧的功能也没有改变。
这么做有利于兼容旧代码,但缺点是 JavaScript 创造者的任何错误或不完善的决定也将永远被保留在 JavaScript 语言中。
这种情况一直持续到 年 ECMAScript 5 (ES5) 的出现。ES5 规范增加了新的语言特性并且修改了一些已经存在的特性。为了保证旧的功能能够使用,大部分的修改是默认不生效的。你需要一个特殊的指令 ——"use strict"
来明确地激活这些特性。
"use strict";// 代码以现代模式工作...
"use strict"
可以被放在函数体的开头。这样则可以只在该函数中启用严格模式。但通常人们会在整个脚本中启用严格模式。
请确保"use strict"
出现在脚本的最顶部,否则严格模式可能无法启用。只有注释可以出现在"use strict"
的上面。如果严格模式一旦启用,就没法中途取消。
这里的严格模式就没有被启用:
alert("some code");// 下面的 "use strict" 会被忽略,必须在最顶部。"use strict";// 严格模式没有被激活
控制台使用
在控制台中输入代码,默认不启动严格模式,解决方案:
在控制台中输入多行代码(使用shift+enter)
(function() {'use strict';// ...你的代码...})()
4.变量
变量是数据的“命名存储”,可以使用变量来存储信息,创建变量时,使用let或者var关键字
//1let a;a='hello world';alert(a);//2let b = 'hello';alert(b);//3let x=1,y=2;
一个变量只能被声明一次,多次重复声明会导致error
error:
SyntaxError: 'message' has already been declared
var let const
var在所有的版本中都可以使用,但是let和const只能在es6及更高的版本使用var的声明作用域:var声明的变量会成为包含他的函数的局部变量,比如:使用var在函数内部声明一个变量,在函数退出时将被销毁。
function test(){var message = "hello";}test();console.log(message); //出错
也可以不加var直接声明变量,这样就能获得一个全局变量,
function sum(){message = "hello";}
但是并不建议这么做,不好维护
再声明时,使用var声明的变量会自动提升到函数作用域顶部
function foo() {console.log(age);var age = 25; //不会报错}//以上代码等同于function foo() {var age;console.log(age);age = 25;}foor() //undefined
let声明的范围是块作用域,var是函数作用域
if(true){let age = 26;console.log(age); //26}console.log(age) //age没有定义
块作用域是函数作用域的自己。
let也不允许出现重复的冗余声明,但是var可以
注:
1.let声明的变量不会在作用域中被提升。在let声明之前的执行瞬间被称为"暂时性死区",在此阶段引用任何后面才声明的变量都会抛出referenceerror错误
2.使用let在全局作用域中声明的变量不会 成为window对象的属性(var声明的变量则会)
3.for循环中最好使用let
const声明时必须初始化变量,而且之后不能修改这个变量的值
const不允许重复声明
声明的作用域也是块作用域
不能再for循环中使用const,属性最自增,出现报错,在for…in和for…of循环中可以使用
在使用const声明对象的时候可以修改对象内部属性
5.变量命名
在命名方面有两个限制
变量名称必须仅包含字母,数字,符号$
和_
首字符必须非数字
一般会使用驼峰命名法,例如userName
对于大小写敏感,tom和TOM是两个不同的变量
也可以使用其他语言命名,比如中文,但不推荐
正确命名:
let username = 'tom';let $ = 'hello'
错误命名:
let 2a = 'world';let my-name='tom'; //连字符 - 不能被用作命名
不能使用保留字命名,会报错
关于变量声明:以下的写法也是正确的,这是为了兼容旧版本,但是在严格模式下会报错
num=5;
6.常量
声明一个常数(不变)变量,可以使用const
而非let
:
const a = '111';
使用const
声明的变量称为“常量”。它们不能被修改,如果修改就会报错
一个普遍的做法是将常量用作别名,以便记住那些在执行之前就已知的难以记住的值。
const COLOR_RED = "#F00";
什么时候该为常量使用大写命名,什么时候进行常规命名?让我们弄清楚一点。
作为一个“常数”,意味着值永远不变。但是有些常量在执行之前就已知了(比如红色的十六进制值),还有些在执行期间被“计算”出来,但初始赋值之后就不会改变。
const pageLoadTime = /* 网页加载所需的时间 */;
pageLoadTime
的值在页面加载之前是未知的,所以采用常规命名。但是它仍然是个常量,因为赋值之后不会改变。
换句话说,大写命名的常量仅用作“硬编码(hard-coded)”值的别名。
7.数据类型
在 JavaScript 中有 8 种基本的数据类型(注:7 种原始类型和 1 种引用类型)。
我们可以将任何类型的值存入变量。例如,一个变量可以在前一刻是个字符串,下一刻就存储一个数字:
// 没有错误let message = "hello";message = 123456;
允许这种操作的编程语言,例如 JavaScript,被称为“动态类型”(dynamically typed)的编程语言,意思是虽然编程语言中有不同的数据类型,但是你定义的变量并不会在定义后,被限制为某一数据类型。
Number类型
const sum = 123;sum=1.22;
number类型可以表示整数和浮点数,可以进行加减乘除操作,在小数点后面没有数字的时候,数值会变成整数。
对于非常大的浮点数,可以实用科学技术法表示
let floatNum1 = 3.125e7 //等于31250000
浮点数的精确度最高可达到17位小数,但是计算不如整数精准,所以在计算0.1+0.2的时候会等于0.30000000000000004.
最大值与最小值都存储在Number.MIN_VALUE和Number.MAX_VALUE中
另外,Infinity
和-Infinity
和NaN
也属于number类型。
Infinity的意思是无穷大。
可以通过1/0来得到Infinity,
alert(1/0);
或者在代码中直接使用
alert( Infinity ); // Infinity
NaN
代表计算错误,他是一个不正确的或者一个未定义的数学操作所得到的结果,比如:
alert( "not a number" / 2 ); // NaN,这样的除法是错误的
NaN
是粘性的。任何对NaN
的进一步操作都会返回NaN
:
alert( "not a number" / 2 ); // NaN
所以,如果在数学表达式中有一个NaN
,会被传播到最终结果。
函数isNaN()可以用于判断参数是否“不是数值”,任何不能转换为数值的值都会导致这个函数返回true。
BigInt类型
在 JavaScript 中,number
类型无法表示大于(2^53-1)
(即9007199254740991
),或小于-(2^53-1)
的整数。这是其内部表示形式导致的技术限制。
BigInt
类型用于表示任意长度的整数。
可以通过将n
附加到整数字段的末尾来创建BigInt
值。
// 尾部的 "n" 表示这是一个 BigInt 类型const bigInt = 1234567890123456789012345678901234567890n;
String类型
JavaScript 中的字符串必须被括在引号里。
字符串的长度可以通过length属性来获取
字符串不可以改变,想要改变先要销毁原来的字符串,然后重新创建
在string类型中,有三种形式
单引号双引号反引号
双引号和单引号都是“简单”引用,在 JavaScript 中两者几乎没有什么差别。
反引号是功能扩展引号。它们允许我们通过将变量和表达式和函数包装在${…}
中,来将它们嵌入到字符串中。例如:
let name = "John";// 嵌入一个变量alert( `Hello, ${name}!` ); // Hello, John!// 嵌入一个表达式alert( `the result is ${1 + 2}` ); // the result is 3
${…}中的表达式会被计算,{}中可以放入变量或者数学表达式等等。插入的值会使用toString()被强制转换为字符串
反引号还允许跨行定义字符串
let muString = `first linesecond line`;
模板字符串标签函数
可以自定义插值行为,标签函数会接受被插值几号分隔后的模板和每个表达式求值的结果。
let a = 6;let b = 9;function simpleTag(strings,avalexpression,bvalexpression,sumexpression){console.log(strings);console.log(avalexpression);console.log(bvalexpression);console,log(sumexpression);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"
原始字符串
使用模板字面量也可以获取原始的字面量内容,使用String.raw()标签函数,可以忽略/n换行符,但是对于真正的换行不能获取,还是会换行
Boolean类型
boolean类型包含两个 值,true和false(区分大小写的)
let nameChecked = true;let sexChecked = false;//也可以作为比较的结果let isGreater = 4 > 1;
Null类型
Null类型包含一个值,这个值是null,
特殊的Null
类型不属于上述任何一种类型。
它构成了一个独立的类型,只包含null
值:
let age = null;alert(typeof age); //弹出objectconsole.log(null == undefined) // trueconsole.log(null === undefined) // false
相比较于其他编程语言,JavaScript 中的null
不是一个“对不存在的object
的引用”或者 “null 指针”。
Null表示的是一个空对象的指针。
JavaScript 中的null
仅仅是一个代表“无”、“空”或“值未知”的特殊值。
上面的代码表示age
是未知的。
Undefined类型
特殊值undefined
和null
一样自成类型,Undefined类型也包含一个值,是undefined
undefined
的含义是未被赋值。
如果一个变量已被声明,但未被赋值,那么它的值就是undefined
:
let age;alert(age); // 弹出 "undefined"
从技术上讲,可以显式地将undefined
赋值给变量:
let age = 100;// 将值修改为 undefinedage = undefined;alert(age); // "undefined"
但是不建议这样做。通常,使用null
将一个“空”或者“未知”的值写入变量中,而undefined
则保留作为未进行初始化的事物的默认初始值。
注:当变量未声明时,输出此变量时会报错,而不是undefined;
但是当使用typeof判断此未声明的变量时是undefined
let name;alert(name); //弹出undefinedalert(na); //报错;na is not definedalert(typeof name); //弹出undefinedalert(typeof na); //弹出undefined
object类型
对象就是一组数据和功能的集合,
let o = new Object()
每个Object实例都有如下属性和方法:
constructor:用于创建当前对象的函数hasOwnProperty(propertyName):用于 判断当前对象实例上是否存在给定的属性,属性名必须是字符串isPrototypeof(object):用于判断当前对象是否为另一个对象的原型propertyIsEnumerable(propertyName):用于判断给定的属性是否可以使用,属性名必须为字符串toLocaleString():返回对象的字符串表示,该字符串反应对象所在的本地化执行环境toString():返回对象的字符串表示valueOf():返回对象对应的字符串,数值,或布尔值表示
Symbol类型(尚未完结)
根据规范,对象的属性键只能是字符串类型或者 Symbol 类型。不是 Number,也不是 Boolean,只有字符串或 Symbol 这两种类型。
“Symbol” 值表示唯一的标识符。
创建:
// id 是 symbol 的一个实例化对象let id = Symbol();
创建时,我们可以给 Symbol 一个描述(也称为 Symbol 名),这在代码调试时非常有用:
// id 是描述为 "id" 的 Symbollet id = Symbol("id");
symbol不能与new一起作为构造函数使用,这样做为了避免创建符号包装对象。
Symbol 保证是唯一的。即使我们创建了许多具有相同描述的 Symbol,它们的值也是不同。描述只是一个标签,不影响任何东西。
例如,这里有两个描述相同的 Symbol —— 它们不相等:
let id1 = Symbol("id");let id2 = Symbol("id");alert(id1 == id2); // false
Symbol 不会被自动转换为字符串
JavaScript 中的大多数值都支持字符串的隐式转换。例如,我们可以alert
任何值,都可以生效。Symbol 比较特殊,它不会被自动转换。
例如,这个alert
将会提示出错:
let id = Symbol("id");alert(id); // 类型错误:无法将 Symbol 值转换为字符串。
如果想要使用alert,可以调用toString()函数
隐藏属性
Symbol 允许我们创建对象的“隐藏”属性,代码的任何其他部分都不能意外访问或重写这些属性。
例如,如果我们使用的是属于第三方代码的user
对象,我们想要给它们添加一些标识符。
我们可以给它们使用 Symbol 键:
let user = {// 属于另一个代码name: "John"};let id = Symbol("id");user[id] = 1;alert( user[id] ); // 我们可以使用 Symbol 作为键来访问数据
使用Symbol("id")
作为键,比起用字符串"id"
来有什么好处呢?
因为user
对象属于其他的代码,那些代码也会使用这个对象,所以我们不应该在它上面直接添加任何字段,这样很不安全。但是你添加的 Symbol 属性不会被意外访问到,第三方代码根本不会看到它,所以使用 Symbol 基本上不会有问题。
另外,假设另一个脚本希望在user
中有自己的标识符,以实现自己的目的。这可能是另一个 JavaScript 库,因此脚本之间完全不了解彼此。
然后该脚本可以创建自己的Symbol("id")
,像这样:
// ...let id = Symbol("id");user[id] = "Their id value";
我们的标识符和它们的标识符之间不会有冲突,因为 Symbol 总是不同的,即使它们有相同的名字。
……但如果我们处于同样的目的,使用字符串"id"
而不是用 symbol,那么就会出现冲突:
let user = {name: "John" };// 我们的脚本使用了 "id" 属性。user.id = "Our id value";// ……另一个脚本也想将 "id" 用于它的目的……user.id = "Their id value"// 砰!无意中被另一个脚本重写了 id!
字面量中的symbol
如果我们要在对象字面量{...}
中使用 Symbol,则需要使用方括号把它括起来。
就像这样:
let id = Symbol("id");let user = {name: "John",[id]: 123 // 而不是 "id":123};
这是因为我们需要变量id
的值作为键,而不是字符串 “id”。
symbol在for…in中被跳过
Symbol 属性不参与for..in
循环。
例如:
let id = Symbol("id");let user = {name: "John",age: 30,[id]: 123};for (let key in user) alert(key); // name, age (no symbols)// 使用 Symbol 任务直接访问alert( "Direct: " + user[id] );
Object.keys(user)
也会忽略它们。这是一般“隐藏符号属性”原则的一部分。如果另一个脚本或库遍历我们的对象,它不会意外地访问到符号属性。
相反,object.assign会同时复制字符串和 symbol 属性:
let id = Symbol("id");let user = {[id]: 123};let clone = Object.assign({}, user);alert( clone[id] ); // 123
全局symbol
全局 Symbol 注册表。我们可以在其中创建 Symbol 并在稍后访问它们,它可以确保每次访问相同名字的 Symbol 时,返回的都是相同的 Symbol。
要从注册表中读取(不存在则创建)Symbol,请使用Symbol.for(key)
。
该调用会检查全局注册表,如果有一个描述为key
的 Symbol,则返回该 Symbol,否则将创建一个新 Symbol(Symbol(key)
),并通过给定的key
将其存储在注册表中。
// 从全局注册表中读取let id = Symbol.for("id"); // 如果该 Symbol 不存在,则创建它// 再次读取(可能是在代码中的另一个位置)let idAgain = Symbol.for("id");// 相同的 Symbolalert( id === idAgain ); // true
注册表内的 Symbol 被称为 全局 Symbol。如果我们想要一个应用程序范围内的 Symbol,可以在代码中随处访问
symbol.keyFor
Symbol.keyFor(sym)
,它的作用完全反过来:通过全局 Symbol 返回一个名字。
// 通过 name 获取 Symbollet sym = Symbol.for("name");let sym2 = Symbol.for("id");// 通过 Symbol 获取 namealert( Symbol.keyFor(sym) ); // namealert( Symbol.keyFor(sym2) ); // id
Symbol.keyFor
内部使用全局 Symbol 注册表来查找 Symbol 的键。所以它不适用于非全局 Symbol。如果 Symbol 不是全局的,它将无法找到它并返回undefined
。
也就是说,任何 Symbol 都具有description
属性。
let globalSymbol = Symbol.for("name");let localSymbol = Symbol("name");alert( Symbol.keyFor(globalSymbol) ); // name,全局 Symbolalert( Symbol.keyFor(localSymbol) ); // undefined,非全局alert( localSymbol.description ); // name
系统symbol
JavaScript 内部有很多“系统” Symbol,我们可以使用它们来微调对象的各个方面。
它们都被列在了symbol表的规范中:
Symbol.hasInstance
Symbol.isConcatSpreadable
Symbol.iterator
Symbol.toPrimitive
……等等。
例如,Symbol.toPrimitive
允许我们将对象描述为原始值转换。
typeof运算符
typeof运算符返回参数的类型,支持两种语法形式
运算符 typeof x函数形式 typeof(x)
typeof的返回值为字符串形式
typeof undefined // "undefined"typeof 0 // "number"typeof 10n // "bigint"typeof true // "boolean"typeof "foo" // "string"typeof Symbol("id") // "symbol"typeof {} //"object"typeof [] //"object"typeof function(){} //"function"typeof Math // "object" (1)typeof null // "object" (2)typeof alert // "function" (3)
额外说明:
Math是一个内置的object对象
typeof null
的结果是"object"
。这是官方承认的typeof
的行为上的错误,这个问题来自于 JavaScript 语言的早期,并为了兼容性而保留了下来。null
绝对不是一个object
。null
有自己的类型,它是一个特殊值。
typeof alert
的结果是"function"
,因为alert
在 JavaScript 语言中是一个函数。在 JavaScript 语言中没有一个特别的 “function” 类型。函数隶属于object
类型。但是typeof
会对函数区分对待,并返回"function"
。这也是来自于 JavaScript 语言早期的问题。
8.交互
alert
他会在浏览器上弹出一条信息,并且等待用户按下确认
alert('hello');
弹出的这个带有信息的小窗口被称为模态窗。“modal” 意味着用户不能与页面的其他部分(例如点击其他按钮等)进行交互,直到他们处理完窗口。在上面示例这种情况下 —— 直到用户点击“确定”按钮。
prompt
prompt函数接受两个参数:
result = prompt(title, [default]);
浏览器会显示一个带有文本消息的模态窗口,还有 input 框和确定/取消按钮。
title
:显示给用户的文本
default
:可选的第二个参数,指定input框的初始值
访问者可以在提示输入栏中输入一些内容,然后按“确定”键。然后我们在result
中获取该文本。或者他们可以按取消键或按 Esc 键取消输入,然后我们得到null
作为result
。
prompt
将返回用户在input
框内输入的文本,如果用户取消了输入,则返回null
。
let result = prompt('你的年龄是多少?');alert(result);
注意:
第二个参数是可选的。但是如果我们不提供的话,Internet Explorer 会把"undefined"
插入到 prompt。
我们可以在 Internet Explorer 中运行下面这行代码来看看效果:
let test = prompt("Test");
所以,为了 prompt 在 IE 中有好的效果,最好始终提供第二个参数:
let test = prompt("Test", ''); // <-- 用于 IE 浏览器
confirm
confirm
函数显示一个带有question
以及确定和取消两个按钮的模态窗口。
点击确定返回true
,点击取消返回false
。
let result = confirm(question);
var sum = confirm('are you children?')alert(sum);
这些方法都是模态的:它们暂停脚本的执行,并且不允许用户与该页面的其余部分进行交互,直到窗口被解除。
上述所有方法共有两个限制:
模态窗口的确切位置由浏览器决定。通常在页面中心。窗口的确切外观也取决于浏览器。我们不能修改它。
以上三个函数都是基于 window对象的。例如 window.alert() window.confirm() window.prompt()
9.类型转换
大多数情况下,运算符和函数会自动将赋予它们的值转换为正确的类型。
比如,alert
会自动将任何值都转换为字符串以进行显示。算术运算符会将值转换为数字。
在某些情况下,我们需要将值显式地转换为我们期望的类型。
字符串转换
两个方法
toString() null和undefined没有toString()方法,接收一个参数用来指定转换为什么进制的字符串String(),它遵循以下规则 1.如果值有toString()方法,则调用该方法2.如果值是null,返回"null"3.如果值是undefined,返回“undefined”
当我们需要一个字符串形式的值时,就会进行字符串转换。
比如,alert(value)
将value
转换为字符串类型,然后显示这个值。
也可以显式地调用String(value)
来将value
转换为字符串类型:
let value = true;alert(typeof value); // "boolean"value = String(value); // 现在,值是一个字符串形式的 "true"alert(typeof value); // "string"
数字类型转换
在算术函数和表达式中,会自动进行 number 类型转换。
一共三个函数:
Number()parseInt() //主要用于字符串转换为数值
最前面的空格会被忽略,从第一个非空格字符开始转换,如果第一个字符不是shuzhizifu.jiahao减号等等,会理解返回NaN。而Number会返回0。此函数也可以自动检测其余进制,也接受第二个参数,用于指定进制数
parseFloat() //主要用于字符串转换为数值
此函数只能转换十进制,遇到其他进制的数字会返回0,还可以检测小数点,但是只检测第一个小数点,遇到第二个小数点的时候会直接返回
比如,当把除法/
用于非 number 类型:
alert("6"/"2"); //string类型会被自动转换为number类型进行计算
也可以使用Number(value)
显式地将这个value
转换为 number 类型。
let str = "123";alert(typeof str); // stringlet num = Number(str); // 变成 number 类型 123alert(typeof num); // number
如果从 string 类型源(如文本表单)中读取一个值,但期望输入一个数字时,通常需要进行显式转换。
如果该字符串不是一个有效的数字,转换的结果会是NaN
;如果该字符串里面全都是数字,则可以正常转换(注意使用双引号)例如:
let age = Number("let us go home")console.log(age) //NaN,转换失败let num = Number("1");console.log(num); //1
Number 类型转换规则
例如:
console.log(Number(undefined)); //NaNconsole.log(Number(null)); //0console.log(Number(true)); //1console.log(Number(false));//0console.log(Number("123z"));//Nanconsole.log(Number("123"));//123
布尔类型转换
它发生在逻辑运算中,但是也可以通过调用 Boolean(value) 显式地进行转换。
转换规则:
直观上为“空”的值(如0
、空字符串、null
、undefined
和NaN
)将变为false
其他值变成true
console.log(Boolean(1)); //trueconsole.log(Boolean(0)); //falseconsole.log(Boolean("1")); //trueconsole.log(Boolean("hello")); //trueconsole.log(Boolean("")); //false//注意:任何非空的字符串都是truealert( Boolean("0") ); // truealert( Boolean(" ") ); // 空白,也是 true(任何非空字符串都是 true)
10.基础运算符
术语:
运算元——运算符应用的对象。比如说乘法运算5 * 2
,有两个运算元:左运算元5
和右运算元2
。有时候人们也称其为“参数”而不是“运算元”。
一元运算符——如果一个运算符对应的只有一个运算元,那么它是一元运算符。比如说加减符号+
,-
如果一个运算符拥有两个运算元,那么它是二元运算符。减号还存在二元运算符形式:
let x = 1, y = 3;alert( y - x ); // 2,二元运算符减号做减运算
数学
加法+
,
减法-
,
乘法*
,
除法/
,
取余%
,
alert(a%b) //结果为a整除b的余数
求幂**
.
alert(2**4); //结果为2的4次方alert(4 ** (1/2) ); //也可以分数作为参数,4开二次方
运算符的其他作用
1.二元运算符+连接字符串
+还可以用于连接字符串
let s = "my" + "string";alert(s); // mystring
只要任意一个运算元是字符串,那么另一个运算元也将被转化为字符串。
alert( '1' + 2 ); // "12"alert( 2 + '1' ); // "21"
alert(2 + 2 + '1' ); // "41",不是 "221"
在这里,运算符是按顺序工作。第一个+
将两个数字相加,所以返回4
,然后下一个+
将字符串1
加入其中,所以就是4 + '1' = 41
。
二元+
是唯一一个以这种方式支持字符串的运算符。其他算术运算符只对数字起作用,并且总是将其运算元转换为数字。
alert( 6 - '2' ); // 4,将 '2' 转换为数字alert( '6' / '2' ); // 3,将两个运算元都转换为数字
2.数字转化,一元运算符 +
一元运算符加号,或者说,加号+
应用于单个值,对数字没有任何作用。但是如果运算元不是数字,加号+
则会将其转化为数字。效果和Number()一样
// 对数字无效let x = 1;alert( +x ); // 1let y = -2;alert( +y ); // -2// 转化非数字alert( +true ); // 1alert( +"" ); // 0
如果在遇到例如表单获取数字的情况下,获取到的是字符串,要将之转换为数字进行操作,可以这样:
let a = "1";let b = "2";alert(+a + +b); //此时a和b都会被转换为数字,并且进行相加结果为3//第二种写法alert(Number(a)+Number(b)); //结果为3
运算符优先级
“一元加号运算符”的优先级是17
,高于“二元加号运算符”的优先级13
。这也是为什么表达式"+a + +b"
中的一元加号先生效,然后才是二元加法。
赋值运算符
赋值运算符的优先级只有3,所以在类如x=2*3+1
的表达式中最后将结果赋值给x
在对同一个变量进行运算时,
let a = 2;a = a + 1;a = a*3;
可以使用运算符+=和*=来缩写以上写法
let a = 2;a += 1; //a等于3a *= 3; //a等于9
所有算术和位运算符都有简短的“修改并赋值”运算符:/=
和-=
等。
这类运算符的优先级与普通赋值运算符的优先级相同,所以它们在大多数其他运算之后执行:
let n = 2;n *= 3 + 5;alert( n ); // 16 (右边部分先被计算,等同于 n *= 8)
链式赋值
let a, b, c;a = b = c = 2 + 2;//从右往左计算alert( a ); // 4alert( b ); // 4alert( c ); // 4
但是最好改为:
c = 2 + 2;b = c;a = c;
自增自减
自增自减只能用于变量
自增:++
将变量与1相加
let count = 2;count++;alert(count) //输出3
自减:将变量减去1
let sum = 2;sum--;alert(sum); //输出1
运算符++
和--
可以置于变量前,也可以置于变量后。
当运算符置于变量后,被称为“后置形式”:counter++
。当运算符置于变量前,被称为“前置形式”:++counter
。
我们知道,所有的运算符都有返回值。前置形式返回一个新的值,但后置返回原来的值(做加法/减法之前的值)。
但是当值未被使用,那么两者没有区别。
let count = 2;let a = count++;alert(a); //2,返回的是旧值let sum = 2;let b = ++sum;alert(b); //3,返回的是新值//----------------------------------------let c = 2;c++;alert(c); //3,没返回let d = 2;++d;alert(d); //3,没返回//-------------------------------------let e = 2;console.log(e++); //2,返回之前的值let f = 2;console.log(++f); //3,返回新的值
位运算符
位运算符把运算元当做 32 位整数,并在它们的二进制表现形式上操作。
按位与 (&
)按位或 (|
)按位异或 (^
)按位非 (~
)左移 (<<
)右移 (>>
)无符号右移 (>>>
)
逗号运算符
逗号运算符能让我们处理多个语句,使用,
将它们分开。每个语句都运行了,但是只有最后的语句的结果会被返回。逗号运算符的优先级很低,比=
还要低
let a = (1 + 2, 3 + 4);alert( a ); // 7(3 + 4 的结果)
逗号运算符的使用场景:
for (a = 1, b = 3, c = a * b; a < 10; a++) {...}
11.值的比较
运算符:
大于 / 小于:a > b
,a < b
。大于等于 / 小于等于:a >= b
,a <= b
。检查两个值的相等:a == b
,请注意双等号==
表示相等性检查,而单等号a = b
表示赋值。检查两个值不相等。写成a != b
。
比较类型为Boolean
比较运算符会返回布尔值
alert( 2 > 1 ); // true(正确)alert( 2 == 1 ); // false(错误)alert( 2 != 1 ); // true(正确)
和其他类型的值一样,比较的结果可以被赋值给任意变量:
let result = 5 > 4; // 把比较的结果赋值给 resultalert( result ); // true
字符串比较
在比较字符串的大小时,JavaScript 会使用“字典”或“词典”顺序进行判定。也是unicode编码。
换言之,字符串是按字符(母)逐个进行比较的。
alert( 'Z' > 'A' ); // truealert( 'Glow' > 'Glee' ); // truealert( 'Bee' > 'Be' ); // true
字符串的比较规则:从第一个字符开始比较,如果第一个相同,则比较下一个,如果不相同,第一个字符大的为大,循环往复。(注:小写字符的unicode编码比大写字符的大)
不同类型之间的比较
当对不同类型的值进行比较时,JavaScript 会首先将其转化为数字(number)再判定大小。
alert( '2' > 1 ); // true,字符串 '2' 会被转化为数字 2alert( '01' == 1 ); // true,字符串 '01' 会被转化为数字 1
对于布尔值,true会被转换为1,false转换为0
注意:有时候,以下两种情况会同时发生:
若直接比较两个值,其结果是相等的。若把两个值转为布尔值,它们可能得出完全相反的结果,即一个是true
,一个是false
。
let a = 0;alert( Boolean(a) ); // falselet b = "0";alert( Boolean(b) ); // truealert(a == b); // true!//在比较的时候会把字符串转换为数字进行比较
严格相等
普通的相等性检查==
存在一个问题,它不能区分出0
和false
也同样无法区分空字符串和false
alert( 0 == false ); // truealert( '' == false ); // true
因为在比较不同类型的值时,处于相等判断符号==
两侧的值会先被转化为数字。空字符串和false
也是如此,转化后它们都为数字 0。
严格相等运算符===
在进行比较时不会做任何的类型转换。严格不相等”表示为!==
。
alert( 0 === false ); // false,因为被比较值的数据类型不同
对null和undefined进行比较
当使用严格相等===
比较二者时,不相等,因为属于不同的类型
alert( null === undefined ); // false
当使用非严格相等==
比较二者时,相等
alert( null == undefined ); // true
当使用数学式或其他比较方法< > <= >=
时:
null/undefined
会被转化为数字:null
被转化为0
,undefined
被转化为NaN
。
比较null和0
alert( null > 0 ); // (1) falsealert( null == 0 ); // (2) falsealert( null >= 0 ); // (3) true
因为相等性检查==
和普通比较符> < >= <=
的代码逻辑是相互独立的。进行值的比较时,null
会被转化为数字,因此它被转化为了0
。这就是为什么(3)中null >= 0
返回值是 true,(1)中null > 0
返回值是 false。
另一方面,undefined
和null
在相等性检查==
中不会进行任何的类型转换,它们有自己独立的比较规则,所以除了它们之间互等外,不会等于任何其他的值。这就解释了为什么(2)中null == 0
会返回 false。
undefined
undefined不应该与其他任何值作比较
alert( undefined > 0 ); // false (1)alert( undefined < 0 ); // false (2)alert( undefined == 0 ); // false (3)
(1)
和(2)
都返回false
是因为undefined
在比较中被转换为了NaN
,而NaN
是一个特殊的数值型值,它与任何值进行比较都会返回false
。
(3)
返回false
是因为这是一个相等性检查,而undefined
只与null
相等,不会与其他值相等。
12.条件分支
if
let a = 4;if(a == 4){alert("a=4");}
最好使用花括号将代码包起来,即使只有一行代码
布尔转换
if (…)
语句会计算圆括号内的表达式,并将计算结果转换为布尔型。
转换规则:
数字0
、空字符串""
、null
、undefined
和NaN
都会被转换成false
其他值被转换为true
if(1){... //此处代码会永远执行}if(0){... //此处代码永远不会执行}let boo = (a == 1);if(boo){... //此处会判断boo的值,如果为true,则会执行,此处代码 }
if…else
if(a==1){... //如果a=1执行此处代码}else if(a==2){... //如果a=2执行此处代码}else{... //如果a不等于1也不等于2,则执行此处代码}
?条件运算符
?也被称为三元运算符
let result = condition ? value1 : value2;let accessAllowed = (age > 18) ? true : false;//下面的代码效果等同let accessAllowed = age > 18;
多个?运算符
let age = prompt('age?', 18);let message = (age < 3) ? 'Hi, baby!' :(age < 18) ? 'Hello!' :(age < 100) ? 'Greetings!' :'What an unusual age!';alert( message );
第一个问号检查age < 3
。如果为真 — 返回'Hi, baby!'
。否则,会继续执行冒号":"
后的表达式,检查age < 18
。如果为真 — 返回'Hello!'
。否则,会继续执行下一个冒号":"
后的表达式,检查age < 100
。如果为真 — 返回'Greetings!'
。否则,会继续执行最后一个冒号":"
后面的表达式,返回'What an unusual age!'
。
?运算符也可以代替if…else使用,但是可读性较差。
13.逻辑运算符
JavaScript 里有三个逻辑运算符:||
(或),&&
(与),!
(非)。
虽然它们被称为“逻辑”运算符,但这些运算符却可以被应用于任意类型的值,而不仅仅是布尔值。它们的结果也同样可以是任意类型。
||(或)
如果参与运算的任意一个参数为true
,返回的结果就为true
,否则返回false
。
如果操作数不是布尔值,那么它将会被转化为布尔值来参与运算。
if (1 || 0) {// 工作原理相当于 if( true || false )alert( 'truthy!' );}
另外(拓展用法):
result = value1 || value2 || value3;
从左到右依次计算操作数。处理每一个操作数时,都将其转化为布尔值。如果结果是true
,就停止计算,返回这个操作数的初始值。如果所有的操作数都被计算过(也就是,转换结果都是false
),则返回最后一个操作数。返回的值是操作数的初始形式,不会做布尔转换。
也就是,一个或运算"||"
的链,将返回第一个真值,如果不存在真值,就返回该链的最后一个值。
alert( 1 || 0 ); // 1(1 是真值)alert( null || 1 ); // 1(1 是第一个真值)alert( null || 0 || 1 ); // 1(第一个真值)alert( undefined || null || 0 ); // 0(所有的转化结果都是 false,返回最后一个值)
以上会引起或运算符的拓展用法:
获取变量列表或者表达式的第一个真值。
let firstName = "";let lastName = "";let nickName = "SuperCoder";alert( firstName || lastName || nickName || "Anonymous"); // SuperCoder
短路求值
||
对其参数进行处理,直到达到第一个真值,然后立即返回该值,而无需处理其他参数。
如果操作数不仅仅是一个值,而是一个有副作用的表达式,例如变量赋值或函数调用,
true || alert("not printed");false || alert("printed");
在第一行中,或运算符||
在遇到true
时立即停止运算,所以alert
没有运行。
有时,人们利用这个特性,只在左侧的条件为假时才执行命令。
&&与
result = a && b;//当两个操作数都为真的时返回true,否则返回false
运算的操作数可以是任意类型的值:
if (1 && 0) {// 作为 true && false 来执行alert( "won't work, because the result is falsy" );}
另外:
result = value1 && value2 && value3;
从左到右依次计算操作数。将处理每一个操作数时,都将其转化为布尔值。如果结果是false
,就停止计算,并返回这个操作数的初始值。如果所有的操作数都被计算过(也就是,转换结果都是true
),则返回最后一个操作数。
换句话说,与运算符返回第一个假值,如果没有假值就返回最后一个值。
// 如果第一个运算符是真值,// 与操作返回第二个操作数:alert( 1 && 0 ); // 0alert( 1 && 5 ); // 5// 如果第一个运算符是假值,// 与操作直接返回它。第二个操作数被忽略alert( null && 5 ); // nullalert( 0 && "no matter what" ); // 0
与运算&&
的优先级比或运算||
要高。
所以代码a && b || c && d
完全跟&&
表达式加了括号一样:(a && b) || (c && d)
。
!(非)
!表示布尔非运算
result = !value;
将操作数转化为布尔类型:true/false
。返回相反的值。
两个非运算!!
有时候用来将某个值转化为布尔类型:
alert( !!"non-empty string" );
14.空值合并运算符
空值合并运算符??
提供了一种简短的语法,用来获取列表中第一个“已定义”的变量(译注:即值不是null
或undefined
的变量)。
a ?? b
的结果是:
a
,如果a
不是null
或undefined
,b
,其他情况。
所以,x = a ?? b
是下面这个表达式的简写:
x = (a !== null && a !== undefined) ? a : b;
另外:
或运算符||
可以与??
运算符以同样的方式使用。我们可以用||
替换上面示例中的??
,也可以获得相同的结果。
重要的区别是:
||
返回第一个真值。??
返回第一个已定义的值。
当我们想将null/undefined
与0
区别对待时,这个区别至关重要。
例如:
let height = 0;alert(height || 100); // 100alert(height ?? 100); // 0
在这个例子中,height || 100
将值为0
的height
视为未设置的(unset),与null
、undefined
以及任何其他假(falsy)值同等对待。因此得到的结果是100
。
height ?? 100
仅当height
确实是null
或undefined
时才返回100
。因此,alert
按原样显示了height
值0
。
哪种行为更好取决于特定的使用场景。当高度0
为有效值时,??
运算符更适合。
拓展:
??运算符的优先级很低,只有5
let height = null;let width = null;// 重要:使用括号let area = (height ?? 100) * (width ?? 50);alert(area); // 5000
出于安全原因,禁止将??
运算符与&&
和||
运算符一起使用。
//这个限制无疑是值得商榷的,但是它被添加到语言规范中是为了避免编程错误,因为人们开始使用 ?? 替代 ||。let x = 1 && 2 ?? 3; // Syntax error//可以使用括号来解决这个问题let x = (1 && 2) ?? 3; // 起作用alert(x); // 2
15.循环
while循环
while循环:
while (condition) {// 代码// 所谓的“循环体”}//当 condition 为 true 时,执行循环体的 code。
如果上述示例中没有i++
,那么循环(理论上)会永远重复执行下去。实际上,浏览器提供了阻止这种循环的方法,我们可以通过终止进程,来停掉服务器端的 JavaScript。
do…while循环
do {// 循环体} while (condition);
循环首先执行循环体,然后检查条件,当条件为真时,重复执行循环体。
for循环
for (begin; condition; step) {// ……循环体……}//例如for (let i = 0; i < 3; i++) {// 结果为 0、1、2alert(i);}
for…in循环
for…In语句是一种严格的迭代语句,用于美剧对象中的非符号键属性,
for (property in expression) statement
举例:
for(const propName in window){document.write(propName);}
对象的属性是无顺序的,所以在输出时对象的顺序可能因浏览器会有些差异
如果ofr-in循环要迭代的是null或undefined,则不会执行循环体
for-of循环
用于遍历可迭代的对象的元素
for (property of expression) statement
举例:
for(const el of [1,2,3,4]){document.write(el);}
如果变量不支持迭代,则会抛出错误
省略语句段
for
循环的任何语句段都可以被省略。
let i = 0; // 我们已经声明了 i 并对它进行了赋值for (; i < 3; i++) {// 不再需要 "begin" 语句段alert( i ); // 0, 1, 2}//该循环与 while (i < 3) 等价。let i = 0;for (; i < 3;) {alert( i++ );}//下面为无限循环for (;;) {// 无限循环}
跳出循环
通常条件为假时,循环会终止。
但我们随时都可以使用break
指令强制退出。
let sum = 0;while (true) {let value = +prompt("Enter a number", '');if (!value) break; // (*)sum += value;}alert( 'Sum: ' + sum );
如果用户输入空行或取消输入,在(*)
行的break
指令会被激活。它立刻终止循环,将控制权传递给循环后的第一行,即,alert
。
继续循环
continue
指令是break
的“轻量版”。它不会停掉整个循环。而是停止当前这一次迭代,并强制启动新一轮循环(如果条件允许的话)。
for (let i = 0; i < 10; i++) {//如果为真,跳过循环体的剩余部分。if (i % 2 == 0) continue;alert(i); // 1,然后 3,5,7,9}
禁止break/continue
在 ‘?’ 的右边
非表达式的语法结构不能与三元运算符?
一起使用。特别是break/continue
这样的指令是不允许这样使用的。
例如:
if (i > 5) {alert(i);} else {continue;}//用问号重写(i > 5) ? alert(i) : continue; // continue 不允许在这个位置//代码会停止运行,并报错
break和continue标签
有时候我们需要从一次从多层嵌套的循环中跳出来。
标签是在循环之前带有冒号的标识符:
outer: for (let i = 0; i < 3; i++) {for (let j = 0; j < 3; j++) {let input = prompt(`Value at coords (${i},${j})`, '');// 如果是空字符串或被取消,则中断并跳出这两个循环。if (!input) break outer; // (*)// 用得到的值做些事……}}alert('Done!');
上述代码中,break outer
向上寻找名为outer
的标签并跳出当前循环。
因此,控制权直接从(*)
转至alert('Done!')
。
continue标签同理,并且只能在for循环内部才能使用break和continue,并且标签只能在指令上方。
16.switch语句和with语句
switch
switch
语句可以替代多个if
判断。
switch
语句有至少一个case
代码块和一个可选的default
代码块。
switch(x) {case 'value1': // if (x === 'value1')...[break]case 'value2': // if (x === 'value2')...[break]default:...[break]}
比较x
值与第一个case
(也就是value1
)是否严格相等
,然后比较第二个case
(value2
)以此类推。如果相等,switch
语句就执行相应case
下的代码块,直到遇到最靠近的break
语句(或者直到switch
语句末尾)。如果没有符合的 case,则执行default
代码块(如果default
存在)。
举例:
let x = 4;switch(x){case 1:alert('1');break;case 4:alert('4');break;default:alert('0');}
如果没有break
,程序将不经过任何检查就会继续执行下一个case
。
任何表达式都可以成为switch和case的参数。例如:+a,b+2等等
如果想要两个条件都执行相同的代码,可以进行case
分组
case 3: // (*) 下面这两个 case 被分在一组case 5:alert('Wrong!');alert("Why don't you take a math class?");break;
with
with语句的用途是将代码作用域设置为特定的对象,
with (expression) statement
使用场景:
针对一个对象反复操作,这时将代码作用域设置为该对象能提高便利
let qs = location.search.substring(1);let hostname = location.hostname;let url = location.href;with(location){let qs = search.substring(1);let hostname = hostname;let url = href;}
注:严格模式下不允许使用with
17.函数
函数是程序的主要“构建模块”。函数使该段代码可以被调用很多次,而不需要写重复的代码。
函数声明
function showMessage() {alert( 'Hello everyone!' );}//函数调用showMessage();
局部变量
只在函数内部可以使用
function showMessage() {let message = "Hello, I'm JavaScript!"; // 局部变量alert( message );}showMessage(); // Hello, I'm JavaScript!alert( message ); // <-- 错误!变量是函数的局部变量
外部变量
函数也可以访问外部变量
函数对外部变量拥有全部的访问权限。函数也可以修改外部变量。
let userName = 'John';function showMessage() {let message = 'Hello, ' + userName;alert(message);}showMessage(); // Hello, John
如果在函数内部声明了同名变量,那么函数会遮蔽外部变量
参数
我们可以使用参数(也称“函数参数”)来将任意数据传递给函数。
我们有一个变量from
,并将它传递给函数。请注意:函数会修改from
,但在函数外部看不到更改,因为函数修改的是复制的变量值副本:
function showMessage(from, text) {from = '*' + from + '*'; alert( from + ': ' + text );}let from = "Ann";showMessage(from, "Hello"); // *Ann*: Hello// "from" 值相同,函数修改了一个局部的副本。alert( from ); // Ann
如果未提供参数,那么其默认值则是undefined
。这并不是错误。假设text===undefined
function showMessage(from, text = "no text given") {alert( from + ": " + text );} //现在如果 text 参数未被传递,它将会得到值 "no text given"。
也可使用默认值
// 如果没有传入 "count" 参数,则显示 "unknown"function showCount(count) {alert(count ?? "unknown");}showCount(0); // 0showCount(null); // unknownshowCount(); // unknown
返回值
函数可以将一个值返回到调用代码中作为结果。
function sum(a, b) {return a + b;}
指令return
可以在函数的任意位置。当执行到达时,函数停止,并将值返回给调用代码(分配给上述代码中的result
)。
只使用return
但没有返回值也是可行的。但这会导致函数立即退出。
空值的return
或没有return
的函数返回值为undefined
如果我们想要将返回的表达式写成跨多行的形式,那么应该在return
的同一行开始写此表达式。或者至少按照如下的方式放上左括号:
return (some + long + expression+ or +whatever * f(a) + f(b))
函数命名
函数就是行为(action)。所以它们的名字通常是动词。它应该简短且尽可能准确地描述函数的作用。这样读代码的人就能清楚地知道这个函数的功能。
一个函数应该只包含函数名所指定的功能,而不是做更多与函数名无关的功能。
18.函数表达式
在 JavaScript 中,函数不是“神奇的语言结构”,而是一种特殊的值。
创建函数的语法称为函数表达式。
let sayHi = function() {alert( "Hello" );}; //注意,此处有分号
我们可以打印sayHi,结果为这个函数的源码 alert(sayHi);这样不会执行函数,因为后面没有括号
我们可以复制函数到其他变量:
function sayHi() {// (1) 创建alert( "Hello" );}let func = sayHi; // (2) 复制func(); // Hello// (3) 运行复制的值(正常运行)!sayHi(); // Hello //这里也能运行(为什么不行呢)
(1)
行声明创建了函数,并把它放入到变量sayHi
。(2)
行将sayHi
复制到了变量func
。请注意:sayHi
后面没有括号。如果有括号,func = sayHi()
会把sayHi()
的调用结果写进func
,而不是sayHi
函数本身。现在函数可以通过sayHi()
和func()
两种方式进行调用。
回调函数
function ask(question, yes, no) {if (confirm(question)) yes()else no();}function showOk() {alert( "You agreed." );}function showCancel() {alert( "You canceled the execution." );}// 用法:函数 showOk 和 showCancel 被作为参数传入到 askask("Do you agree?", showOk, showCancel);
ask
的两个参数值showOk
和showCancel
可以被称为回调函数或简称回调。
下面是大幅的简写
function ask(question, yes, no) {if (confirm(question)) yes()else no();}ask("Do you agree?",function() {alert("You agreed."); }, //匿名函数function() {alert("You canceled the execution."); });
函数表达式VS函数声明
函数表达式:
函数表达式是在代码执行到达时被创建,并且仅从那一刻起可用
一旦代码执行到赋值表达式let sum = function…
的右侧,此时就会开始创建该函数,并且可以从现在开始使用(分配,调用等)。而不能够在之前调用等
函数声明:
在函数声明被定义之前,它就可以被调用。
例如,一个全局函数声明对整个脚本来说都是可见的,无论它被写在这个脚本的哪个位置。
这是内部算法的原故。当 JavaScript准备运行脚本时,首先会在脚本中寻找全局函数声明,并创建这些函数。我们可以将其视为“初始化阶段”。
在处理完所有函数声明后,代码才被执行。所以运行时能够使用这些函数。
(也就是说javascipt会先初始化创建函数,之后再执行js脚本)
函数声明的另外一个特殊的功能是它们的块级作用域。
严格模式下,当一个函数声明在一个代码块内时,它在该代码块内的任何位置都是可见的。但在代码块外不可见。
let age = prompt("What is your age?", 18);// 有条件地声明一个函数if (age < 18) {function welcome() {alert("Hello!");}} else {function welcome() {alert("Greetings!");}}// ……稍后使用welcome(); // Error: welcome is not defined
另一个例子
let age = 16; // 拿 16 作为例子if (age < 18) {welcome();// \ (运行)// |function welcome() {// |alert("Hello!"); // | 函数声明在声明它的代码块内任意位置都可用} // |// |welcome();// / (运行)} else {function welcome() {alert("Greetings!");}}// 在这里,我们在花括号外部调用函数,我们看不到它们内部的函数声明。welcome(); // Error: welcome is not defined
我们怎么才能让welcome
在if
外可见呢?
正确的做法是使用函数表达式,并将welcome
赋值给在if
外声明的变量,并具有正确的可见性。
let age = prompt("What is your age?", 18);let welcome;if (age < 18) {welcome = function() {alert("Hello!");};} else {welcome = function() {alert("Greetings!");};}welcome(); // 现在可以了
19.箭头函数
它被称为“箭头函数”,因为它看起来像这样:
let func = (arg1, arg2, ...argN) => expression//举例let sum = (a, b) => a + b;//如果参数只有一个let double = n => n * 2;//如果没有参数let sayHi = () => alert("Hello!");//以上写法都需要函数调用
let age = prompt("What is your age?", 18);let welcome = (age < 18) ?() => alert('Hello') :() => alert("Greetings!");welcome();
let sum = (a, b) => {// 花括号表示开始一个多行函数let result = a + b;return result; // 如果我们使用了花括号,那么我们需要一个显式的 “return”};alert( sum(1, 2) ); // 3
20.垃圾回收
可达性
javascript中的内存管理概念是可达性
“可达”值是那些以某种方式可访问或可用的值。它们一定是存储在内存中的。
这里列出固有的可达值的基本集合,这些值明显不能被释放。
1.比方说:
当前函数的局部变量和参数。嵌套调用时,当前调用链上所有函数的变量与参数。全局变量。(还有一些内部的)
这些值被称作根(roots)。
2.如果一个值可以通过引用或引用链从根访问任何其他值,则认为该值是可达的。
比方说,如果全局变量中有一个对象,并且该对象有一个属性引用了另一个对象,则该对象被认为是可达的。而且它引用的内容也是可达的。
avaScript 引擎中有一个被称作 垃圾回收期 的东西在后台执行。它监控着所有对象的状态,并删除掉那些已经不可达的。
// user 具有对这个对象的引用let user = {name: "John"};//如果user被重写,那么对于本来对象的引用就消失了,垃圾回收器就会将对象内存释放//user = null
相互关联的对象
例子:
function marry(man, woman) {woman.husband = man;man.wife = woman;return {father: man,mother: woman}}let family = marry({name: "John"}, {name: "Ann"});
marry
函数通过让两个对象相互引用使它们“结婚”了,并返回了一个包含这两个对象的新对象。
到目前为止,所有对象都是可达的。
现在移除两个引用:
delete family.father;delete family.mother.husband;
对外引用不重要,只有传入引用才可以使对象可达。所以,John 现在是不可达的,并且将被从内存中删除,同时 John 的所有数据也将变得不可达。
几个对象相互引用,但外部没有对其任意对象的引用,这些对象也可能是不可达的,并被从内存中删除。
即没有变量对应对象的引用。
内部算法
垃圾回收的基本算法被称为 “mark-and-sweep”。
定期执行以下“垃圾回收”步骤:
垃圾收集器找到所有的根,并“标记”(记住)它们。然后它遍历并“标记”来自它们的所有引用。然后它遍历标记的对象并标记 他们的引用。所有被遍历到的对象都会被记住,以免将来再次遍历到同一个对象。……如此操作,直到所有可达的(从根部)引用都被访问到。没有被标记的对象都会被删除。
一些优化建议:
分代收集—— 对象被分成两组:“新的”和“旧的”。许多对象出现,完成它们的工作并很快死去,它们可以很快被清理。那些长期存活的对象会变得“老旧”,而且被检查的频次也会减少。增量收集—— 如果有许多对象,并且我们试图一次遍历并标记整个对象集,则可能需要一些时间,并在执行过程中带来明显的延迟。所以引擎试图将垃圾收集工作分成几部分来做。然后将这几部分会逐一进行处理。这需要它们之间有额外的标记来追踪变化,但是这样会有许多微小的延迟而不是一个大的延迟。闲时收集—— 垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响。
21.作用域链和上下文
变量或函数的上下文决定了他们可以访问那些数据,以及他们的行为,
每个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都会挂载到这个变量对象上。但是无法通过代码来访问变量对象
最外层的上下文是全局上下文,在浏览器中,全局上下文就是我们所说的window对象,因此定义的全局变量和函数都会变成window对象的属性和方法。
使用let和const的顶级声明不会定义在全局上下文中,但在作用域链上的解析效果是一样的,上下文在所有代码执行完毕后才会被销毁,定义在上面的变量和函数也会同时被摧毁,而全局上下文会在整个应用程序退出之后才会销毁,比如关闭浏览器。
每个函数调用都有自己的上下文,当代码执行到函数时,函数的上下文会被推到一个上下文栈上,在函数执行完毕后,上下文会弹出这个函数的上下文。
在上下文的代码执行的时候,会创建一个变量对象的一个作用域链。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文的变量对象始终都会在作用域链的最顶端。
如果上下文是函数,那么函数的活动对象(变量对象最开始只有一个定义变量:arguments)用作变量对象,其余不变。
var color = "blue";function changeColor() {let anotherColor = "red";function swapColors() {let tempColor = another;another = color;color = tempColor;//这里可以访问color,anotherColor和tempColor}//这里可以访问color,anotherColor,但访问不到tempColorswapColors();}//这里只能访问到color;changeColor();
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lpfcyHfQ-1603639997485)(C:\Users\刘东旭\Desktop\javascript笔记\images\作用域链.jpg)]
如图所示:下面的变量对象可以访问上面的,但是反过来不行,也就是说,最里层的一直都可以访问外层的上下文。
注意:函数参数被认为是当前上下文 中的变量,因此也跟上下文中的其他变量遵循相同的访问规则
作用域链增强
执行上下文主要有全局上下文和函数上下文,但是可以通过其他方式来增强作用域链。某些语句会导致作用域链前端临时添加一个上下文,这个上下文在代码执行后会被删除,有两种情况会出现:
try…catch中的catch块
会创建一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明
with语句
向作用域前端添加指定的对象
function buildUrl() {let qs = "?debug = true";with(location){let url = href+qs;}return url;}
这里,with语句将location对象作为上下文,因此location会被添加到作用域链的前端
当with中引用href时,其实引用的是location.href,也就是自己对象的属性。