JS高级程序设计
什么是JavaScript?
JavaScript的实现
完整的JavaScript包含
- 核心(ECMAScript)
可见JS和ES的区别,ES不局限于浏览器。其只是对实现了这个规范描述的所有方面的一门语言的称呼 - 文档对象模型(DOM)
- 浏览器对象模型(BOM)
ES6增加了什么?
ES6正式支持了类、模块、迭代器、生成器、箭头函数、期约、反射、代理和众多新的数据类型
DOM
文档对象模型(DOM,Document Object Model)是一个应用程序接口,用于在HTML中使用扩展的XML
注意:DOM并非只能通过JavaScript访问,如SVG、MathML、SMIL等语言
BOM
浏览器对象模型(Browser Object Model),用于支持访问和操作浏览器的接口。
BOM主要针对浏览器窗口和子窗口,不过人们通常会把任何特定于浏览器的扩展都归于BOM的范畴内,比如:
- 弹出新浏览器窗口的能力
- 移动、缩放和关闭浏览器窗口的能力
- navigator对象,提供关于浏览器的详尽信息
- location对象,提供浏览器加载页面的详尽信息
- screen对象,提供关于用户屏幕分辨率的详尽信息
- performance对象,提供浏览器内存占用、导航行为和时间统计的详尽信息
- 对cookie的支持
- 其它自定义的对象
HTML中的JavaScript
<script>元素
将JavaScript插入HTML中主要方法是使用<script>,它有八个属性
- async:表示应该立即开始下载脚本,但不能阻止其他页面动作
- charset:指定代码字符集
- crossorigin:配置相关请求的CORS(跨域资源共享设置),默认不使用
- defer:表示脚本可以延迟到文档完全被解析和显示后再执行,只对外部脚本文件有效
- integrity:允许比对接收到的资源和指定的加密签名以验证子资源完整性。
- language:废弃,最初用于表示代码块中的脚本语言
- src:表示包含要执行的代码的外部文件
- type:代替language,表示代码块中脚本语言的内容类型
一些注意事项:
- 代码中不能出现字符串'</script>',比如
function sayHi(){
console.log('</script>'); //会导致浏览器报错
}
//但可以通过转义字符避免这个问题
function sayHi(){
console.log('\</script>'); //会导致浏览器报错
}
- 在解释外部JavaScript文件时,与解释行类JavaScript文件一样页面会发生阻塞(阻塞时间也包含下载文件的时间)
- 使用了src属性的<script>元素不应该在<script>和</script>标签中再包含其它JavaScript代码。如果两者都提供,浏览器会忽略行内代码
- <script>标签可以包含外部域的JavaScript文件。其src与<img>标签很像,可以是一个完整的URL,并且这个URL指向的资源可以与它包含的HTML不在一个域中。浏览器在解析这个资源时,会向src指定的URL发送一个GET请求
标签位置
现代WEB应用程序通常将JavaScript引用放在<body>元素中的页面后面,以防止解析时间过长带来的页面空白问题
推迟执行脚本
给<script>标签添加defer属性,告诉浏览器立即下载,但延迟执行,会在DOMContentLoaded事件之前执行
异步执行脚本
给<script>标签添加async属性,也会立即下载,与defer不同的是,标记为async的脚本并不能保证按照他们出现的次序执行
动态加载脚本
通过在DOM里面动态添加script元素document.createElement('script')',
行内代码与外部文件
通常推荐使用外部文件的方式引入JavaScript,理由如下
- 可维护性。将JavaScript文件集中起来更有利于维护。
- 缓存。如果两个页面用到同一个JavaScript文件,只需要下载一次,加载更快。
- 适应未来
文档模式
- 混杂模式
- 标准模式
- 准标准模式
<noscript>元素
在两种情况下,浏览器将显示<noscript>标签里的内容
- 浏览器不支持脚本
- 浏览器对脚本的支持被关闭
语言基础
严格模式
对于es3中的一些不规范写法进行处理,对于不安全的活动将抛出错误。
启用严格模式:"use strict"
可以作用于整个文档或是单个函数
关键字与保留字
关键字:不能作为标识符和属性名
| break | do | in | typeof |
|---|---|---|---|
| case | else | instanceof | var |
| catch | export | new | void |
| class | extends | return | while |
| const | finally | super | with |
| continue | for | switch | yield |
| debugger | function | this | default |
| if | throw | delete | import |
| try |
保留字:不能作为标识符,但可以作为属性名(最好不要)
- 始终保留:enum
- 严格模式下保留:implements,package,public,interface,protected,static,let,private
- 模块代码中保留:await
变量
var关键字
- var的声明作用域
function test(){
var message = "hi"; //局部变量
}
test();
console.log(message); //出错
在上面的代码中,message是一个在函数内部定义的局部变量,在函数退出时即被销毁
不过,在定义变量时可以省去var操作符,这样就会得到一个全局变量
function test(){
message = "hi"; //局部变量
}
test();
console.log(message); //hi
只要调用一次test,就会定义message这个变量
注意:不推荐使用var定义全局变量,在局部作用域下定义的全局变量很难维护,也会造成困惑,不能一下子断定出省略var是不是有意而为之。在严格模式下,这样声明变量会抛出ReferenceError
- var声明提升
使用var声明的变量会自动提升到函数作用域顶部
function foo(){
console.log(age)
var age = 26;
}
foo(); //undefined
//等同于
function foo(){
var age;
console.log(age)
var age = 26;
}
此外,反复使用var声明同一个变量也没有问题
let声明
let与var最明显的区别是,let声明的范围是块作用域,var声明的范围是函数作用域
let不允许在同一个块作用域中出现冗余声明
var name;
let name; //SyntaxError
let name;
var name; //SyntaxError
可见两个关键词声明的并不是不同类型的变量,而是指出变量的相关作用域如何存在
另一个重要区别,就是let声明的变量不会在作用域中被提升
- 暂时性死区
在let声明之前的执行瞬间被称为“暂时性死区”,在此阶段引用任何后面才声明的变量都会抛出ReferenceError - 全局声明
与var不同,使用let在全局作用域中声明的变量不会称为window对象的属性 - 条件声明
在使用var声明变量时,多余的变量声明会在作用域顶部合并为一个声明。而因为let作用域是块,所以不可能检查前面是否已经使用let声明国同名变量。 - for循环中的let声明
在let出现之前,for循环定义的迭代变量会渗透到循环体外部
for(var i=0;i<5;i++){
//循环逻辑
}
console.log(i); //5
改成使用let这个问题就消失了,因为迭代变量的作用域仅限于for循环内部
使用var最常见的问题就是对迭代变量的奇特声明和修改
for(var i=0;i<5;++i){
setTimeOut( () => console.log(i) , 0)
}
//并不会输出0、1、2、3、4
//会输出5、5、5、5、5
//原因:在退出循环时,迭代变量保存的是导致循环退出的值:5。所有的i都是同一个变量,因而输出的是同一个值
//而在使用let声明迭代变量时,JavaScript引擎会在后台为每个迭代循环都声明一个新的迭代变量。
const声明
const行为与let基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,并且尝试修改const声明的变量会导致运行时出错修改const引用对象内部的属性并不违反const的限制
不能用于for循环中的迭代变量(因为迭代变量会自增),但在声明一个不会被修改的for循环变量时可以使用。对for-in和for-of很有意义
声明风格和最佳实践
- 不使用var
- 优先使用const,let次之
数据类型
6种简单数据类型
- Undefined
- Null
- Boolern
- Number
- String
- Symbol
typeof 操作符
typeof操作符是为了确定任意变量的数据类型而生。返回一个字符串 (undefined,boolern...)比简单数据类型多了个function
typeof null返回的是object,这是因为特殊值null被认为是一个对空对象的引用
严格来讲,函数在ECMAScript种被认为是对象,但是函数有着自己特殊的属性,因此使用typeof来区分函数和其它对象很重要
undefined类型
当使用let或var声明了变量但没有初始化时,就相当于给变量赋予了undefined值。
无论是声明还是未声明,typeof返回的都是字符串“undefined”。
Null类型
逻辑上来讲,null值表示一个空对象指针,这也是给typeof传一个null会返回object的原因。在将来要保存对象值的变量时,建议使用null来初始化。
console.log(undefined == null) //true ‘==’这个操作符会为了比较而转换操作数
Boolern类型
| 数据类型 | 转换为true的值 | 转换为false的值 |
|---|---|---|
| Boolean | true | false |
| String | 非空字符串 | “”(空字符串) |
| Number | 非0数值 | 0、NAN |
| Object | 任意对象 | null |
| Undefined | N/A(不存在) | undefined |
Number类型
- 八进制数:前缀0,如果字面量包含的数字超过了应有的范围,会忽略前面的0。严格模式下,前缀0会被视为语法错误,应使用0o。
- 十六进制数:前缀0x
浮点值
定义浮点值,数值中必须包含小数点。
1.0 10.00 这种浮点值会被转为整数值1,10
科学计数法:1.23e3 => 1230
浮点值的精确度最高可达17位小数,但在算术计算中远不如整数精确。比如0.1+0.2=0.3000000000000000004.因此永远不要测试某个特定的浮点值。
可以计算时将小数转换为整数:0.1+0.2 => (0.110+0.210)/10 =0.3
值的范围
- 最小数值:
Number.MIN_VALUE:5e-324 - 最大数值:
Number.MAX_VALUE:1.7976931348623157e+308 - 正无穷大:即超过最大的正数值 Infinity Number.NEGATIVE_INFINITY
- 负无穷大:-Infinity Number.POSITIVE_INFINITY
- 可以使用
isFinite()函数来检测一个值是不是无限大
NaN
NaN意为“不是数值”(not a number),用于表示本来要返回数值的操作失败了
比如用0除任何数值在其他语言中通常会导致错误,从而停止执行,而在ECMAScript中会返回NaN
NaN的独特属性
- 任何涉及NaN的操作始终返回NaN
- isNaN()函数可以判断参数是否“不是数值”
console.log(isNaN(NaN)); //true
console.log(isNaN(10)); //false
console.log(isNaN("10")); //false
console.log(isNaN("bule")); //true,无法转换为数值
console.log(isNaN(true)); //true,可以转换为数值1
数值转换
有三个函数可以将非数值转换为数值:Number(),parseInt(),parseFloat()
Number():
| 数值类型 | 返回值 |
|---|---|
| 布尔值 | true为1,false为0 |
| 数值 | 直接返回 |
| null | 返回为0 |
| undefined | 返回NaN |
字符串有点特殊,分为几种情况
- 如果字符串包含字符数值,包括数值字符前面带加、减号的情况,则转换为一个十进制数值
Number('011')会返回十进制数11,想要返回八进制数就将0改为0o - 如果字符串包含有效的浮点值格式如“1.1”,则会返回相应浮点值1.1
- 如果字符串包含有效的十六进制格式如“0xf”,则会返回相应十六进制数
- 如果是空字符串,则返回0
- 除了以上之外的其它字符,都返回NaN
对象:调用valueof()方法,并按照上述规则转换返回的值。如果转换结果是NaN,则调用toString()方法,再按照字符串的规则转换
parseInt()
通常在需要得到整数时可以考虑优先使用parseInt()函数。parseInt()函数会将字符串最前面的空格忽略,从第一个非空格字符开始转换。如果第一个字符不是数值字符、加号或者减号,就会立即返回NaN。意味着空字符串也会返回NaN,这与Number()不一样
let num1=parseInt('blue1'); //NaN
let num2=parseInt('11blue'); //11
let num3=parseInt('11blue11)' //11
let num4=parseInt('0xA') //10 解释为十六进制整数
由于不同的数值格式很容易混淆,因此parseInt()接收第二个参数,用于指定进制数。如果知道要解析的值是十六进制,那么可以传入16作为第二个参数
let num = parseInt('0xAF',16) //175(如果传入了第二个参数,'0x'其实可以省略)
因为不传底数参数相当于让parseInt()自己决定如何解析,所以为了解析出错,建议始终传给它第二个参数
parseFloat()
与parseInt()工作方式相似,都是从位置0开始检测字符,直到解析到字符串末尾或一个无效的浮点字符数值为止。第一次出现的小数点是有效的,之后的都无效并忽略剩余字符
parseFloat()只能解析十进制值,不能指定底数
String类型
字符字面量
| 字面量 | 含义 |
|---|---|
| \n | 换行 |
| \t | 制表 |
| \b | 退格 |
| \r | 回车 |
| \f | 换页 |
| \ | 反斜杠 |
| ' | 单引号 |
| " | 双引号 |
| ` | 反引号 |
| \xnn | 以十六进制编码nn表示的字符,nn是十六进制数字0~F,例如\x41等于“A” |
| \unnnn | 以十六进制编码nnnn表示的Unicode字符,例如\u03a3 |
字符串特点
字符串一旦创建,它们的值就不能变了。要修改某个变量中的字符串值,必须先销毁原始的字符串,然后将包含新值的另一个字符串保存到该变量。
转换为字符串
有两种方法可以把一个值转换为字符串
toString()
几乎所有值都有的方法(null和undefined没有),唯一用途就是返回当前值的字符串等价物。
多数情况下,toString()不接收任何参数,但是在对数值调用这个方法时,可以接收一个底数参数。
如果不确定一个值是不是null或undefined,可以使用String()转型函数。
- 如果值有
toString()方法,则调用该方法(不传参数),并返回结果 - 如果值是null,返回‘null’
- 如果值是undefined,返回‘undefined’
模板字面量
使用反引号定义字符串叫做模板字面量。模板字面量能够保存换行字符,可以跨行定义字符串。在定义HTML模板时特别有用。
字符串插值
模板字面量最常用的一个特性就是支持字符串插值。 ${ javascript句法表达式 } 所有插入的值都会使用toString()强制转型为字符串,任何JavaScript表达式都可以用于插值
模板字面量标签函数
模板字面量也支持标签函数,而通过标签函数可以自定义插值行为。标签函数会接收被插值记号分割后的模板和对每个表达式求值的结果
let a=5;
let b=11;
function simpleTag(strings,...args){
console.log(strings);
for(const str of args){
console.log(str);
}
return 1
}
let tagRes=simpleTag`${a}+${b}=${a+b}`
//[ '', '+', '=', '' ]
//5
//11
//16
console.log(tagRes); //1
对于有n个插值的模板字面量,传给标签函数的表达式参数个数始终是n,而传给标签函数的第一个参数所包含的字符串个数始终是n+1
如果想把这些字符串和对表达式求值的结果拼接起来作为默认返回的字符串,可以这样做:
let a = 6
let b = 9
function zipTag(strings, ...expressions) {
return strings[0] + expressions.map((e, i) => `${e}${strings[i + 1]}`).join('')
}
let untaggedResult = `${a}+${b}=${a + b}`
let taggedResult = zipTag`${a}+${b}=${a + b}`
console.log(untaggedResult); //6+9=15
console.log(taggedResult) //6+9=15
原始字符串
可以使用String.raw()标签函数获取原始的模板字面量内容,而不是转换后的字符显示
// 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
但是对于实际的换行符是不行的,它们不会转换成转义序列的形式
Symbol类型
Symbol是ECMAScript 6新增的数据类型。符号是原始值,且符号实例是唯一,不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险
基本用法
符号需要使用Symbol()函数进行初始化。因为符号本身是原始类型,所以typeof操作符对符号返回symbol。
在调用Symbol()函数时,可以传入一个字符串参数作为对符号的描述,将来可以通过这个字符串来调试代码。但这个字符串跟符号定义或标识完全无关。
let fooSymbol = Symbol('foo')
let otherFooSymbol = Symbol('foo')
console.log(fooSymbol === otherFooSymbol) //false
符号没有字面量语法。创建Symbol()实例并将其作为对象的新属性,就可以保证它不会覆盖已有的对象属性,无论是符号属性还是字符串属性。
另外Symbol()函数不能与new关键字一起作为构造函数使用。这是为了避免创造符号包装对象。
let mySymbol = new Symbol(); //TypeError: Symbol is not a construtor
使用全局符号注册表
如果运行时的不同部分需要共享或重用符号实例,可以用一个字符串作为键,在全局符号注册表中创建并重用符号。为此可以使用Symbol.for()方法。Symbol.for()对每个字符串键都执行幂等操作。第一次使用某个字符串调用时,它会检查全局运行时的注册表,发现不存在对应的符号,于是就会生成一个新符号实例并添加到注册表中。后续使用相同的字符串的调用同样会检查注册表,发现存在与该字符串对应的符号,然后就会返回该符号实例。
let fooGlobalSymbol = Symbol.for('foo') //创建新符号
let otherGlobalSymbol = Symbol.for('foo') //重用已有符号
console.log(fooGlobalSymbol === otherGlobalSymbol) //true
即使采用相同的符号描述,在全局注册表中定义的符号跟使用Symbol()定义的符号也不等同。另外,作为参数传给Symbol.for()的任何值都会被转换为字符串。注册表中使用的键也会被当作符号描述。
let emptyGlobalSymbol = Symbol.for();
console.log(emptyGlobalSymbol); //Symbol(undefined)
还可以使用Symbol.keyFor()来查询全局注册表,这个方法接收符号,返回该全局符号对应的字符串键。如果查询到不是全局符号,则返回undefined。
//创建全局符号
let s = Symbol.for('foo');
console.log(Symbol.keyFor(s)); //foo
//创建普通符号
let s2 = Symbol('foo');
console.log(Symbol.keyFor(s2)) //undefined
如果传给Symbol.for()的不是符号,则该方法抛出TypeError
使用符号作为属性
凡是可以使用字符串或数值作为属性的地方,都可以使用符号。
- Object.getOwnPropertyNames() 返回对象实例的常规属性数组
- Object.getOwnPropertySymbol() 返回对象实例的符号属性数组
- Object.getOwnPropertyDescriptors() 返回同时包含常规和符号属性描述的对象
- Reflect.ownKeys ()会返回两种类型的键 因为符号属性是对内存中符号的一个引用,所以直接创建并用作属性的符号不会丢失。但是如果没有显示地保存对这些属性的引用,那么必须遍历对象的所有符号属性才能找到相应的属性键
常用内置符号
ES6中引入了一些常用内置符号,用于暴露语言内部行为。开发者可以直接访问、重写或模拟这些行为。这些内置符号都以Symbol工厂函数字符串属性的形式存在。内置符号最重要的用途就是重新定义它们,从而改变原生结构的行为。
比如,for-of循环会在相关对象上使用Symbol.iterator属性,那么就可以通过在自定义属性上重新定义Symbol.iterator的值,来改变for-of在迭代该对象时的行为。
- Symbol.asyncIterator:一个方法,该方法返回对象默认的AsyncIterator,由for-await-of语句使用
- Symbol.hasInnstance:一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例,由instanceof操作符使用
- Symbol.isConcatSpreadable:一个布尔值,如果是true,则意味着对象应该用Array.prototype.concat()打平其数组元素。如果是false,则会导致整个对象或数组被追加到数组末尾
- Symbol.iterator:一个方法,该方法返回对象默认的迭代器,由for-of语句使用
- Symbol.match:一个正则表达式方法。该方法用正则表达式去匹配字符串。由String.prototype.match()方法使用
- Symbol.replace:一个正则表达式方法,该方法替换一个字符串中匹配的子串,由String.prototype.replace()方法使用
- Symbol.search:一个正则表达式方法,该方法返回字符串中匹配正则表达式的索引,由String.prototype.search()方法使用
- Symbol.species:一个函数值,该函数作为创建派生对象的构造函数。这个方法在内置类型中最常用,用于对内置类型实例方法的返回值暴露实例化派生对象的方法
- Symbol.split:一个正则表达式方法,该方法在匹配正则表达式的索引位置拆分字符串。由String.prototype.split()方法使用
- Symbol.toPrimitive:一个方法,该方法将对象转换为相应的原始值。由ToPrimitive抽象操作使用。
- Symbol.toStringTag:一个字符串,该字符串用于创建对象的默认字符串描述。由内置方法Object.prototype.toString()使用
- Symbol.unscopables:一个对象,该对象的所有的以及继承的属性,都会从关联对象的with环境绑定中排除(不推荐使用)
Object类型
ECMAScript中的对象其实就是一组数据和功能的集合。对象通过new操作符后跟对象类型的名称来创建。let o = new Object()'()'可以省去,但不推荐。ECMAScript中的object是派生其他对象的基类。每个Object实例都有如下的属性和方法。
- constructor:用于创建当前对象的函数
- hasOwnProperty(propertyName):用于判断当前对象实例上是否存在给定的属性。要检查的属性名必须是字符串或符号
- isPrototypeOf(object):用于判断当前对象是否是另一个对象的原型
- propertyIsEnumerable(propertyName):用于判断给定的属性是否可以使用for-in语句枚举。属性名必须是字符串
- toLocaleString():返回对象的字符串表示,该字符串反映对象所在的本地执行环境。
- toString():返回对象的字符串表示
- valueOf():返回对象对应的字符串、数值或布尔值表示。通常与toString()的返回值相同。
操作符
一元操作符
只操作一个值的操作符叫做一元操作符。
- 递增/递减操作符:++/--,注意前后缀
let s1 = '2';
let s2 = 'z';
let b = false; //对于布尔值,true转换为1,false转换为0再应用改变
let f = 1.1;
let o = { //对于对象,先调用其valueOf(),如果是NaN,再调用其toString()再应用字符串的规则
valueOf() {
return -1
}
};
s1++; //值变为数值3
s2++; //值变为NaN
b++; //值变为数值1
f--; //值变为0.10000000000000009,因为浮点数不精确
o--; //值变为-2
- 一元加和减
一元加如果应用到非数值,则会执行与使用
Number()转型函数一样的类型转换。一元减主要用于把数值变为负值,在应用非数值时也会像一元加那样进行转换,然后再取负值。
位操作符
位操作符并不直接应用到64位表示,而是先把值转换为32位整数,再进行位操作,之后再把结果转换为64位。
有符号整数使用32位的前31位表示整数值(从右到左)。第32位表示数值的符号,0为正,1为负。
负值以补码的二进制编码存储,一个数值的二补数通过如下3个步骤计算得到
- 确定绝对值的二进制表示
- 找到数值的反码(每位按位取反)
- 给结果加1
给特殊值NaN和Infinity在位操作中都会被当成0处理。如果应用到非数值,那么首先会使用Number()函数将该值转换为数值,然后再应用位操作