第1章 Javascript-基础知识
1.1 变量类型与变量计算
1.1.1 值类型与引用类型
值类型
值类型(不会相互影响):undefined、string、number、boolean
1
2
3
4var a=100;
var b=a;
a=200;
console.log(b)//100
引用类型
引用类型:对象、数组、函数
1
2
3
4var a={age:20}
var b=a
b.age=21
console.log(a.age)//21
值类型与引用类型的区别
- 两者之间的一个区别是,原始数据类型是通过值传递的,对象是通过引用传递的。
- 值传递:意味着创建原始文件的副本。把它想象成一对双胞胎:他们出生的时候一模一样,但是双胞胎中的老大在战争中失去了一条腿,而老二却没有。
- 引用传递: 意味着创建原始文件的别名。当我妈妈叫沙雕的时候,虽然我的名字叫小智,但这并不是说我就突然就克隆了一个自己:我仍然是我,只是可以用不同名字来称呼我而已。
- JS中的“严格”模式是什么 当使用严格模式时,不能使用隐式声明的变量,或为只读属性赋值,或向不可扩展的对象添加属性。
值类型和引用类型的提升
1 | function changeAgeAndReference(person){ |
1.1.2 变量
var变量提升
- 即将所有的变量声明都拉到函数作用域的顶部
1
2
3
4
5
6
7
8
9
10
11
12function foo(){
console.log(age) //undefined
var age =12
}
foo()
//之所以不会报错,是因为ECMAScript运行时把它看成等价于如下代码
function foo(){
var age
console.log(age)
age = 12
}
foo() //undefinedlet块作用域
- 暂时性死区
- 在let声明之前的执行瞬间都会抛出错误,这就是与var很大不同
1
2
3
4
5
6
7//name 会被提升
console.log(name) //undefined
var name = 'Matt'
//age 不会被提升
console.log(age); //RefferenceError age没有定义
let age = 26
- 在let声明之前的执行瞬间都会抛出错误,这就是与var很大不同
- 全局声明:let在全局作用域声明的变量不会成为window对象属性(var则会)
const
同let基本相同,声明变量时必须同时初始化变量,且尝试修改const声明的变量会导致运行时错误
声明限制只适用于它指向的变量引用,如果const变量引用的是一个对象,修改对象内部并不违反const的限制
对于迭代循环中使用
不使用var
const优先,let其次使用const声明可以让浏览器运行时强制保持变量不变。只在提前知道未来会修改时,再使用let。
1.1.3 *变量计算
强类型转换
字符串拼接
1
var b = 100+'10' //'10010'
运算符(下面0、null、’’、undefined都转化为false,所有相等,用的时候要小心)
1
2
3100 == '100' //true
0 == '' //true
null == undefined //true
逻辑运算
1 | console.log(10 && 0); //0 |
操作符
一元操作符
递增/递减操作符(++ / –):前缀操作符,会在被求值之前改变;后缀操作符,则会在被求值之后改变。
1
2
3
4
5
6
7
8let num1 = 2
let num2 = 20
let num3 = --num1 + num2
console.log(num1) // 1
console.log(num3) //21
let a = false
console.log(a++) //1
位操作符
按位非(-):对数值取反减1
按位与(&):同为同,异为0
按位或(|):同为同,异为1
按位异或(^):同为0,异为1
左移
1
2let oldValue = 2 //等于二进制10
let newValue = oldValue << 5 //等于二进制1000000即十进制64有/无符号 右移
有符号右移:2个大于号表示,同时保留符号(正或负)
无符号右移:3个大于号表示
1
2let oldValue = 64 //等于二进制 1000000
let newValue = oldValue >>> 5 //等于二进制10,即十进制2
指数操作符
ES7新增了指数操作符,Math.pow()等于**
1
2console.log(Math.pow(3,2)) //9
console.log(3 ** 2) //9指数赋值操作符**=
1
2
3let squared = 3
squared **= 2
console.log(squared) //9
加减操作符
加法操作符
任一操作数是NaN,则返回NaN
Infinity加-Infinity,则返回NaN
其中一个操作数是字符串,则另一操作数也会被转换为字符串,再将两个字符串拼接起来
1
2let result =5 + "5"
console.log(result) //"55"任一操作数是对象、数值或布尔值,则调用它们的toString()方法获取字符串。对于undefined和null,则调用String()函数,分别获取”undefined”和”null”
减法操作符
如果任一操作数是字符串、布尔值、null或undefined,则会使用Number()转为数值。
1
2
3
4
5let result1 = 5 - true // 4
let result2 = NaN - 1 //NaN
let result3 = 5 - "" //5
let result4 = 5 - "2" //3
let result5 = 5 - null //5
相等操作符
表达式 | 结果 |
---|---|
undefined == null | true |
false == 0 | true |
true == 1 | true |
undefined == 0 | false |
null == 0 | false |
“5” == 5 | true |
Js中使用typeof能得到的类型
可以得到类型如下:
1
2
3
4
5
6
7
8
9//问题:JS中使用typeof能得到哪些类型
typeof undefined //undefined
typeof 'abc' // String
typeof 123 //number
typeof true //boolean
typeof {} //object
typeof [] //object
typeof null //object
typeof console.log //funcitontypeof可以得到
undefined、string、number、boolean
可以区分值类型,但对于引用类型无法很细的区分,只能区分函数。
尤其是
typeof null object
,它是一个引用类型
在if里面都会为false
- 0
- NaN
- ‘’
- null
- undefined
何时使用===和==
1 | //问题:何时使用===何时使用== |
JS中的内置函数
1 | //问题:JS中有哪些内置函数----数据封装类对象 |
Number
浮点值:如下两种情况都会转为整数值
- 小数点后没有数字(如:1.)
- 数值本来就是整数,只是小数点后面跟着0(如:1.0)
Number()
- undefined 返回NaN
- null 返回 0
- 字符串 比如’011’返回11,无法转为数值的则返回NaN
- true返回1 false返回0
parseInt()
“124blye” 返回124
“” 返回NaN
“0xA” 解释为十六进制数 返回10
“22.5” 返回22
传入第二个参数的用法
1
2console.log(parseInt(10,2)) //2 按照二进制解析
console.log(paeseInt(10,8)) //8 按照八进制解析
parseFloat()
toString()和String()
- toString方法可见于数值、布尔值、对象和字符串,null和undefined没有该方法
1
2
3let num = 10
console.log(num.toString()) //"10"
console.log(num.toString(2)) //"1010" - String()基本同上,null和undefined有所不同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20console.log(String(null)) //"null"
console.log(String(undefined)) //"undefined"
//使用toString会报错
let a = null
console.log(a.toString()) //Uncaught TypeError:
console.log(b.toString()) //Uncaught ReferenceError:
##### isNaN
- NaN不等于本身
- **isNaN()传入一个参数,首先会尝试将其转为数值,无法转为数值返回true**
```js
console.log(isNaN(10)) //false
console.log(isNaN("10") //false
console.log(isNaN(blue) //true
console.log(isNaN(true) //false
- toString方法可见于数值、布尔值、对象和字符串,null和undefined没有该方法
Infinity
- 任何无穷大的正数用Infinity表示,负数用-Infinity
Math
舍入方法
Math.ceil() 向上取舍
Math.floor() 向下取舍
Math.round() 四舍五入
Math.fround() 返回数值最接近的单精度(32位)浮点值表示
JSON
1 | //问题:如何理解JSON |
1.2 执行上下午与作用域
执行上下文
1 | console.log(a); // undefined |
注意⚠️“函数声明”和“函数表达式”的区别
1
2
3
4
5
6
7
8
9
10fn()
function fn() {
//函数声明
}
fn1()
var fn1 = function () {
//函数表达式
}
var a = 100; //类似于这个
创建10个标签 点击的时候弹出来对应的序号
错误写法
1
2
3
4
5
6
7
8
9
10
11
12//这是一个错误的写法!!!
var i,a;
for (var i = 0; i < 10; i++) {
a = document.createElement('a');
a.innerHTML = i + '<br>';
a.addEventListener('click',function (e) {
e.preventDefault();
alert(i)
})
document.body.appendChild(a);
}
//输出为如下: <a>"9"<br></a>正确写法
1
2
3
4
5
6
7
8
9
10
11
12
13//这是一个正确写法!!!
var i;
for (i = 0; i < 10; i++) {
(function(i){
var a = document.createElement('a');
a.innerHTML = i + '<br>';
a.addEventListener('click',function (e) {
e.preventDefault();
alert(i);
})
document.body.appendChild(a);
})(i)
}
1.3 基本数据类型
JavaScript(ES6)中,现在有6种基本数据类型
Undefined、Null、Boolean、Number、String、Symbol
Number
- toFixed() 方法返回包含指定小数点位数的数值字符串
1
2let num = 10
console.log(num.toFixed(2)) //"10.00" - ES6新增了Number.IsInteger()方法,用于辨别一个数值是否保存为整数
1
2
3console.log(Number.isInteger(1)) //true
console.log(Number.isInteger(1.00)) //true
console.log(Number.isInteger(1.01)) //false
String
常用的几个方法
concat 连接字符串
slice、substr、substring 提取子字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29let stringValue = "hello world"
//正数参数
//一个参数
console.log(stringValue.slice(3)) //"lo world"
console.log(stringValue.substring(3)) //"lo world"
console.log(stringValue.substr(3)) //"lo world"
//两个参数
console.log(stringValue.slice(3,7)) //"lo w"
console.log(stringValue.substring(3,7)) //"lo w"
//与上不同
//第二个参数表示返回子字符串长度
console.log(stringValue.substr(3,7)) // "lo worl"
//负数参数
//一个参数
//长度加上负参数 下面相当于8
console.log(stringValue.slice(-3)) //"rld"
//负数转化为0
console.log(stringValue.substr(-3)) "rld"
console.log(substring(-3)) "hello wolrd"
//两个参数
//第二个参数等于长度加上该负数即3到7
console.log(stringValue.slice(3,-4))//"lo w"
//第二个负数转为0 即0到3
console.log(stringValue.substring(3,-4))//"hel"
//第二个负数转为0 代表字符串长度为0 即空字符串
console.log(stringValue.substr(3,-4)) //""indexOf()、lastIndexOf() 定位子字符串,两者区别在于一个是在字符串开头查找,一个是末尾查找
1
2
3
4
5
6
7let stringValue = "hello world"
console.log(stringValue.indexOf("o")) //4
console.log(stringValue.lastIndexOf("o")) //7
//第二个参数代表开始搜索的位置
console.log(stringValue.indexOf("o",6)) //7
console.log(stringValue.lastIndex("o",6)) //4startsWith()、endsWith()和includes(),区别在于startsWith()检查开始于索引0,endsWith()反之,而includes()检查整个字符串
1
2
3
4
5
6
7//startsWith和includes方法的第二个参数,表示开始搜索的位置
let message = "foobarbaz"
console.log(message.startsWith("foo",1)) //false
console.log(message.includes("bar",4)) //false
//endsWith第二个参数,表示应该当作字符串末尾的位置,默认不传则是字符串长度
console.log(message.endsWith("bar",6)) //truetrim()清除字符串前后包含的空格,返回的是字符串的副本,原字符串不受影响,trimLeft和trimRight分别用于从字符串的开始和末尾清除空字符串
1
2
3
4let stringValue = " hello world "
let trimmedStringValue = stringValue.trim()
console.log(trimmedStringValue) //"hello world"
console.log(stringValue) " hello world "repeat方法接收一个整数参数,表示要将字符串复制多少次,然后返回拼接所有副本后的结果
padStart和padEnd方法用于复制字符串,第一个参数是长度,第二个参数是可选填充字符串
1
2
3
4
5
6
7
8
9
10
11
12let stringValue = "foo"
console.log(stringValue.padStart(6)) //" foo"
console.log(stringValue.padStart(9,".")) //"......foo"
console.log(stringValue.padEnd(6)) //"foo "
console.log(stringValue.padEnd(9,".")) //"foo......"
//如提供的字符串多余,则会将其拼接并截断姨匹配指定长度,此外长度小于或者等于字符串长度,则会返回原字符串
console.log(stringValue.padStart(8,"bar")) //"barbafoo"
console.log(stringValue.padStart(2)) //"foo"
console.log(stringValue.padEnd(8,"foo")) //"foobarba"字符串的解构
1
2let message = "abcde"
console.log([...message]) //["a","b","c","d","e"]字符串大小写转换toLowerCase()、toLocaleLowerCase()、toUpperCase()和toLocaleUpperCase(),如果不知道代码涉及什么语言,最好使用地区特定转换方法
字符串匹配模式
match()
1
2
3
4
5
6let text = "cat, bat, sat, fat"
let pattern = /.at/
//等价于pattern.exec(text)
let matches = text.match(pattern)
console.log(matches[0]) //"cat"search() 返回第一个匹配的位置索引,如果没有找到则返回-1
1
2let pos = text.search(/at/)
console.log(pos) //1replace()接收两个参数,第一个参数可以是RegExp对象或一个字符串,第二个参数可以是一个字符串或函数。如果第一个参数是字符串,只会替换第一个子字符串,要想替换所有子字符串,必须用正则并且带全局标记
1
2
3
4
5let result = text.replace("at","ond")
console.log(result) //"cond, bat, sat, fat"
result = text.replace(/at/g, "ond")
console.log(result) //"cond, bond, sond, fond"第二个参数是字符串的情况下,有几个特殊的字符序列,可以用来插入正则表达式操作的值
1
2let result = text.replace(/(.at)/g,"word ($1)")
console.log(result) //word (cat), word (bat), word (sat), word (fat)第二个参数是函数的情况下,函数会接收三个参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function htmlEscape(text){
return text.replace(/[<>"&]/g,function(match,pos,originalText){
switch(match){
case "<":
return "<";
case ">":
return ">";
case "&":
return "&";
case "\":
return """;
}
});
}
console.log(htmlEscape("<p class=\"greeting\">Hello world!</p>"))
//"<p class="greeting">Hello world!</p>"split()
1
2
3
4let colorText = "red,blue,green,yellow"
let colors1 = colorText.split(",")//["red","blue","green","yellow"]
let colors2 = colorText.split(",",2) //["red","blue"]
let colors3 = colorsText.split(/[^,]+/) //["", ",", ",", ",", ""]
1.4 语句
标签语句
- 下面的例子,start是一个标签,可以在后面通过break或continue语句引用
1
2
3start: for(let i = 0;i < count; i++){
console.log(i)
}
break
- break使用标签语句的示例 添加标签不仅让break退出j的内部循环,也会退出i的外部循环,当执行到i和j都等于5时,循环停止执行。
1
2
3
4
5
6
7
8
9
10
11let 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
continue
continue使用标签语句的示例
1
2
3
4
5
6
7
8
9
10
11let 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) //95continue语句会强制循环继续执行,但不是继续执行内部循环,而是继续循环外部,当满足条件时,执行continue到外部循环继续执行,从而导致内部循环少执行5次
第2章 Javascript-引用类型
2.1 Object类型
2.1.1 定义对象
方法一
1
2var person = new Object();
person.name = "Tammy";方法二
1
2
3var person = {
name:"Tammy"
}
2.1.2 简写对象语法
简写对象属性
1
2
3
4
5
6
7
8let name = 'Matt'
let person = {
name: name
}
//等价于上面
let person = {
name
}简写方法名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18let person = {
sayName: function(name) {
console.log('....')
}
}
//等价于上面
let person = {
sayName(name) {
console.log(...)
}
}
//简写方法名可与计算属性相互兼容
const methodKey = 'sayName'
let person = {
[methodKey](name) {
console.log(...)
}
}2.1.3 合并对象Object.assign
Object.assign()实际上对每个源对象执行的是浅复制,它接收一个目标对象和一个或多个源对象作为参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14let dest, src, result
dest = {}
src = {id: 'src'}
result = Object.assign(dest,src)
console.log(dest === result) //true
console.log(dest !== src) //true
console.log(result) //{id: 'src'}
console.log(dest) //{id : 'src'}
//多个源对象
dest = {}
result = Object.assign(dest,{a:'foo'},{b:'bar'})
console.log(result) //{a:'foo',b:'bar'}
2.1.4 相等判定 Object.is
- 有些特殊情况
===
也无能为力,故可以采用Object.is1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17console.log(NaN === NaN) //false
console.log(isNaN(NaN)) //true
//使用Object.is替换===统一写法
console.log(Object.is(NaN,NaN)) //true
#### 2.1.5 简写对象语法
- 简写对象属性
```js
let name = 'Matt'
let person = {
name: name
}
//等价于上面
let person = {
name
} - 简写方法名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18let person = {
sayName: function(name) {
console.log('....')
}
}
//等价于上面
let person = {
sayName(name) {
console.log(...)
}
}
//简写方法名可与计算属性相互兼容
const methodKey = 'sayName'
let person = {
[methodKey](name) {
console.log(...)
}
}2.1.6 对象解构
- 使用
1
2
3
4
5
6
7let person = {
name:'Matt',
age:27
}
//personName、personAge是重命名
let {name: personName, age: personAge} = person
console.log(personName,personAge)// Matt,27 - 如果引用属性不存在则变量值就是undefined
1
2
3
4
5
6let {name, job} = person
console.log(job) //undefined
//可以在解构赋值的同时定义默认值
let {name, job ='engineer'} = person
console.log(job) //engineer - 函数声明在函数体内部使用局部变量
1
2
3
4
5
6
7
8
9
10
11let person = {
name: 'Matt',
age: 27
}
function printPerson(foo, {name:personName,age:personAge},bar){
console.log(arguments)
console.log(personName,personAge)
}
printPerson('1st',person,'2nd')
//['1st',{name:'Matt',age:27}, '2nd']
//'Matt',27
2.1.7 创建对象的几种模式
工厂模式
- 工厂模式特点:使用普通函数,函数内部创建Oject对象,最后返回该对象
1
2
3
4
5
6
7
8
9
10
11
12function createPerson(name, age, job){
let o = new Object()
o.name = name
o.age = age
o.job = job
o.sayName = function(){
console.log(this.name)
}
return o
}
let person1 = createPerson('Matt',29,'engineer')
let person2 = createPerson('Joe',27,'doctor') - 缺点:没有解决对象标识问题,即新创建的对象是什么类型
构造函数模式
- 构造函数模式特点是首字母大写,需要实例化对象
1
2
3
4
5
6
7
8
9
10function Person(name, age, job){
this.name = name
this.age = age
this.job = job
this.sayName = function(){
console.log(this.name)
}
}
let person1 = new Person('Matt',29,'engineer')
let person2 = new Person('Joe',27,'doctor') - 实例化时,如果不传参,构造函数后面的括号可不加
1
2let person1 = new Person()
let person2 = new Person - 缺点:每次定义函数,都会初始化一个对象
原型模式
原型模式优点是定义在它上面的属性和方法可以被对象实例共享
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function Person() {}
Person.prototype.name = 'Nichilas'
Person.prototype.age = 29
Person.prototype.job = 'Software Engineer'
Person.prototype.sayName = function(){
console.log(this.name)
}
let person1 = new Person('Matt',29,'engineer')
person1.sayName() //'Matt'
let person2 = new Person('Joe',27,'doctor')
person2.sayName() //'Joe'
console.log(person1.sayName == person2.sayName) //true
原理如下图:当调用person1.sayName时,js引擎先询问person1的实例有sayName属性吗?找不到的话接下来询问person1的原型有sayName属性吗?答案是有的。
如果给对象实例添加一个属性,这个属性就会遮蔽原型对象上的同名属性,虽然不会修改它,但会屏蔽对它的访问,不过使用delete操作符可以完全删除实例上这个属性。
1
2
3
4
5
6
7
8
9
10
11
12function Person(){}
Person.prototype.name = "Nicholas"
let person1 = new Person()
let person2 = new Person()
person1.name ="Greg"
console.log(person1.name) //"Greg" 来自实例
console.log(person2.name) //"Nicholas" //来自原型
delete person1.name
console.log(person1.name) //"Nicholas" 来自原型判断某个属性是在实例上还是原型上
- hasOwnProperty() 属性存在于调用它的对象实例上返回true
1
console.log(person2.hasOwnProperty("name")) //false
- in 无论存在于原型还是实例对象,只要存在就返回true
1
console.log("name" in person2) //true
- hasOwnProperty() 属性存在于调用它的对象实例上返回true
列举所有实例属性
- Object.getOWnPropertyNames
1
2let keys = Object.getOwnPropertyNames(Person.prototype)
console.log(keys) //"[constructor,name,age,job,sayName]"" - Object.getOwnPropertySymbols()同上,区别在于针对Symbol的key
1
2
3
4
5
6
7let k1 = Symbol("k1"),
k2 = Symbol("k2")
let o = {
[k1]: 'k1',
[l2]: "k2"
}
console.log(Object.getOwnPropertySymbols(o))//[Symbol(k1),Symbol(k2)]
- Object.getOWnPropertyNames
对象迭代Object.values()、Object.entries(),这两个方法执行对象的浅复制
1
2
3
4
5
6
7
8const o ={
foo: 'bar',
baz: 1,
qux: {}
}
console.log(Object.values(o))
//["bar",1,{}]
console.log(Object.entries(o))// [["foo","bar"],["baz",1],["qux",{}]]注意符号Symbol会被忽略
1
2
3
4
5const sym = Symbol()
const o ={
[sym] : 'foo'
}
console.log(Object.values(o)) //[]重写原型
1
2
3
4
5
6
7
8
9
10
11
12
13
14function Person(){}
let friend = new Person()
//重写原型
Person.prototype = {
name: 'Nicholas',
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name)
}
}
//重写构造函数之后再创建实例才会引用新的原型,但是friend是在重写前创建的,故仍然引用最初的原型
friend.sayName() //错误
Person.prototype被设置为一个对象字面量,这种写法完全重写了默认的prototype对象,因此其constructor属性也指向Object,不再指向原来的构造函数,Person.prototype.constructor等于Object而不是Person原型的动态性,任何时候对原型对象的修改也会在实例上反映出来
1
2
3
4
5let friend = new Person()
Person.prototype.sayHi = function(){
console.log('Hi')
}
friend.sayHi() //'Hi'原型缺点:导致所有实例默认取得相同的属性值
1
2
3
4
5
6
7
8
9
10
11
12function Person(){}
Person.prototype ={
friends:["Shelby","Court"]
}
let person1 = new Person()
let person2 = new Person()
person1.friends.push("Van")
console.log(person1.friends) //["Shelby,Court,Van"]
console.log(person2.friends) //["Shelby,Court,Van"]
console.log(person1.friends === person2.friens) //true
2.2 Array类型
2.2.1 Array.from 用于将类数组结构转换为数组实例
字符串会被拆分为单字符数组
1
console.log(Array.from("Matt")) //["M","a","t","t"]
将集合或映射转换为一个新数组
1
2
3
4const m = new Map().set(1,2).set(3,4);
const s = new Set().add(1).add(2).add(3).add(4)
console.log(Array.from(m)) //[[1,2],[3,4]]
console.log(Array.from(s)) //[1, 2, 3, 4]对现有数组执行浅复制
1
2
3
4const a1 = [1, 2, 3, 4]
const a2 = Array.from(a1)
console.log(a1 === a2) //false转换带有必要属性的自定义对象
1
2
3
4
5
6
7
8const arrayLikeObject = {
0: 1,
1: 2,
2: 3,
3: 4,
length: 4
}
console.log(Array.from(arrayLikeOject)) //[1, 2, 3, 4]可接收第二个参数,第三个参数
1
2
3
4
5
6
7const a1 = [1, 2, 3, 4]
const a2 = Array.from(a1, x => x**2) //求x的2次方
const a3 = Array.from(a1, function(x) {
return x**this.exponent
},{exponent: 2})
console.log(a2) //[1, 4, 9, 16]
console.log(a3) //[1, 4, 9, 16]
2.2.2 Array.of 将一组参数转为数组
1 | console.log(Array.of(1, 2, 3, 4)) //[1, 2, 3, 4] |
2.2.3 数组空位
使用数组字面量初始化数组时,可以使用一串逗号创建空位,ES6新增的方法普遍将这些空位当作存在的元素undefined
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15const options = [1,,,,5]
for(const option of options){
console.log(option === undefined)
}
//false
//true
//true
//true
//false
//map会跳过空位置
console.log(options.map(() => 6)) //[6, undefined, undefined, undefined, 5]
//join视空位置为字符串
console.log(option.join('-') //"1---5"由于行为不一致性和存在性能隐患,实践中要避免使用数组空位,确实要使用,可以显式用undefined代替
2.2.4 数组迭代器
keys()、values()、entries()
1
2
3
4
5const a = ["foo", "bar", "baz", "qux"]
const aKeys = Array.from(a.keys()) //[0,1,2,3]
const aValues = Array.from(a.values()) //["foo","bar","baz","qux"]
const aEntries = Array.from(a.entries()) //[[0,"foo"],[1,"bar"],[2,"baz"],[3,"qux"]]
2.2.5 复制copyWithin和填充fill
- fill() 可以向一个已有的数组中插入全部或者部分相同的值
- copyWithin()会按照指定范围浅复制数组中的部分内容
2.2.6 检测数组Array.isArray()
- 确定一个值是不是数组
1
2
3if(Array.isArray(value)){
//操作数组
}
2.2.7 转换方法
- toString()返回数组中每个值的等效字符串拼接而成的一个逗号分割的字符串
1
2let colors = ["red","blue","green"]
console.log(colors.toString()) //red,blue,green
2.2.8 栈方法
- 栈是一种后进先出的结构,也就是最近添加的项先被删除,push和pop只发生在栈顶
1
2
3
4
5
6
7const colors = ["red","blue"]
colors.push("brown")
colors[3] = "black"
alert(colors.length) //4
let item = colors.pop()
alter(item) //black
2.2.9 队列方法
队列是姨先进先出的结构,队列在列表末尾添加数据,但是从列表开头获取数据
shift()会删除数组的第一项并返回它,把shift()和push()可以把数组当队列来使用
1
2
3
4
5
6let colors = new Array()
colors.push("red","green")
colors.push("black")
let item = colors.shift() //"red"
alert(colors.length) //2unshift():在数组开头添加多个值,然后返回新的数组长度。通过使用unshift()和pop(),可以在相反方向上模拟队列,即在数组开头添加新数据,在数组末尾取得数据
1
2
3
4
5
6
7
8
9
10
11let colors = new Array()
let count = colors.unshift("red","green")
alert(count) //2
alert(count) //["red","green"]
count = colors.unshift("black")
alert(count) //3
let item = colors.pop()
alert(item) //green
alert(colors.length) //2
2.2.10 排序方法
- reverse 反向排序
- sort 排序
1
2
3
4
5
6
7let values =[0, 1, 5, 10, 15]
values.sort(compare)
alert(values) //[0,1,5,10,15]
function compare(value1,value2){
return value2 - value1
}
2.2.11 操作方法
concat 连接字符串
slice
1
2
3
4
5
6let colors = ["red", "green", "blue", "yellow", "purple"]
let colors2 = colors.slice(1)
let colors3 = colors.slice(1,4)
console.log(colors2) //["green", "blue", "yellow", "purple"]
console.log(colors3) // ["green", "blue", "yellow"]slice参数有负值,那么数值长度加上这个负值的结果确定位置,在一个5个元素的数组上调用slice(-2,-1),就相当于slice(3,4)。如果结束位置小于开始位置,则返回空数组
splice 删除、插入和替换数组元素,始终返回一个数组,它包含从数组中被删除的元素(如果没有删除元素,则返回空数组)
删除 传两个参数,删除第一个元素的位置和要删除元素的数量。比如splice(0,2) 会删除前两个元素
插入 传3个参数,开始位置、要删除的元素数量和要插入的元素。三个参数之后传第四个第五个乃至任意多个要插入的元素。比如splice(2,0,”red”,”green”)会从数组位置2开始插入字符串”red” 和”green”
替换 删除元素的同时可以在指定位置插入新元素。比如splice(2,0,”red”,”green”) 会从数组位置2开始插入字符串”red”和”green”
2.2.12 搜索和位置方法
indexOf、lastIndexOf和includes
- indexOf和lastIndexOf 接收两个参数,第一个是查找的元素,第二个是开始查找的位置,区别是一个从开头查找一个从末尾查找。返回要查找的元素在数组中的位置,没找到则返回-1。
- includes 返回布尔值
find和findIndex
- find 返回第一个匹配的元素,findIndex返回第一个匹配元素的索引。第二个可选参数用于指定断言函数内部this的值 找到匹配后,两个方法都不会再继续搜索
1
2
3
4
5
6
7
8
9
10
11
12const people = [
{
name: "Matt",
age:27
},
{
name:"Joe",
age:29
}]
alert(people.find((element,index,array) => element.age < 28)) //{name:"Matt",age:27}
alert(people.findIndex((element,index,array)=> element.age < 28)) //0
2.2.13 迭代方法
- every 传入的函数必须对每一项都返回true,它才会返回true
- some 只要有一项让传入的函数返回true,它就会返回true
- filter
- map
- forEach
2.2.14 归并方法
- reduce和reduceRight区别是一个从数组的第一项开始遍历,一个从末尾开始遍历
- 接收两个参数,第一每一项都会运行的归并函数,第二可选的归并起点的初始值
- 归并函数接收4个函数:上一个归并值、当前项、当前项索引和数组本身。这个函数返回的任何值都会作为下一次调用同一个函数的第一个参数。如果没有给初始值(即第二个参数),则第一次迭代将从数组的第二项开始。
1
2
3let values = [1, 2, 3, 4, 5]
let sum = values.reduce((prev,cur,index,array) => prev + cur)
alert(sum) //15
2.3 Date类型
1 | Date.now()//获取当前时间毫秒数 |
2.4 Set类型
介绍
- Set很多方面像是加强的Map,很多API和行为同Map是共有的
API
- 使用new创建空集合
1
const m = new Set()
- 创建并初始化
1
2
3
4
5
6
7
8
9
10
11
12const m1 = new Set(["val1","val2","val3"])
alert(m1.size) //3
//使用自定义迭代器初始化集合
const m2 = new Set({
[Symbol.iterator]:function*(){
yield "val1";
yield "val2";
yield "val3";
}
})
alert(m2.size) //3 - 使用add增加值,has查询,size获取元素数量,delete和clear删除元素
1
2
3
4
5
6
7
8
9
10
11
12const s = new Set()
//多操作串联使用
s.add("Matt").add("Jeo")
alert(s.size) //2
s.has("Matt") //true
s.delete("Matt")
alert(s.size) //1
s.clear()
alert(s.size) //0 - 与Map类似,Set可以包含任何js数据类型作为值。注意的是作为值的对象属性被改变也不会收到影响
1
2
3
4
5
6const s = new Set()
const objVal = {}
s.add(objVal)
objVal.bar = "bar"
alert(s.has(objVal)) //true - delete返回一个布尔值,表示集合中是否存在要删除的值
1
2
3
4
5const s = new Set()
s.add('foo')
alert(s.delete('foo') //true
alert(s.delete('foo') //false
顺序与迭代
- Set会维护值插入时的顺序,支持按顺序迭代
- 可使用values、keys等方法进行迭代,同Map一样
1
2
3//常用把Set集合转换为数组
const s = new Set(["val1","val2","val3"])
console.log([...s]) //["val1","val2","val3"]
WeakSet
- WeakSet是Set的兄弟,其API也是Set的子集。
- 基本
1
const ws = new WeakSet()
- 特点同WeakMap基本一致,区别在于API不同
2.5 Map类型
2.5.1 基本
- 使用new关键字和Map构造函数可以创建一个空映射
1
const m = new Map()
- 创建同时初始化实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23//使用嵌套数组初始化映射
const m1 = new Map([
["key1","val1"],
["key2","val2"],
["key3","val3"]
])
alert(m1.size) //3
//使用自定义迭代器初始化映射
const m2 = new Map({
[Symbol.iterator]: function*(){
yield ["key1","val1"],
yield ["key2","val2"],
yield ["key3","val3"]
}
})
alert(m2.size) //3
//映射期待的键值对,无论是否提供
const m3 = new Map([[]])
alert(m3.has(undefined)) //true
alert(m3.get(undefined) //undefined - set方法添加键值对,get和has查询,size属性获取键值对数量,delete和clear删除值
1
2
3
4
5
6
7
8
9
10
11
12
13const m = new Map()
//多个操作串联使用
m.set("firstName","Matt")
.set("lastName","Joe")
alert(m.has("firstName")) //true
alert(m.get("firstName")) //"Matt"
alert(m.size) //2
m.delete("firstName")
alert(m.size) //1
m.clear()
alert(m.size) //0 - 与Object不同的是,Map可以使用任何js数据类型作为键
2.5.2 迭代
1 | const m = new Map([ |
2.5.3 Object与Map选择
- 给定固定内存大小,Map大约比Object多存储50%的键值对
- 插入、删除数据多,Map性能更优
- 查找数据,包含少量键值对时,Oject有时候速度更快
2.5.4 WeakMap
基本
- WeakMap是Map的兄弟类型,其API也是Map的子集,”weak”表示弱映射的键,这些键不属于正式的引用,会被垃圾回收。但是如果键值对存在于映射中,并被当作对值的引用,就不会被当作垃圾回收
1
2
3
4
5//set方法初始化一个新对象作为一个字符串的键,但是这个对象没有其他引用,当执行完代码后,这个对象键就会被垃圾回收,因为值也没有被引用,值本身也会被垃圾回收。
const wm = new WeakMap()
wm.set({},"val")
//container对象维护着一个对弱映射键的引用,因此这个对象键不会成为垃圾回收的目标,但是如果调用了removeReference,就会摧毁对象的最后一个引用,垃圾回收程序就会把这个键值对清理掉。API
- 使用
1
const wm = new WeakMap()
- 弱映射中的键只能是Oject或者继承自Object的类型,尝试使用非对象设置键会抛出TypeError,值的类型无限制
1
2
3
4
5
6
7
8
9
10
11
12
13const key1 = {id: 1}
const wm2 = new WeakMap([
[key1,"val1"],
["BADKEY","val2"]
])
//TypeError
//原始值可以先包装成对象再用作键
const stringKey = new String("key1")
const wm3 = new WeakMap([
stringKey,"val1"
])
alert(wm3.get(stringKey)) - 特点为不可迭代键,因为WeakMap中的键值对任何时候都可以能被销毁。
使用
- DOM节点元数据,因为WeakMap实例不会妨碍垃圾回收,所以非常适合保存关联元数据。 假设上面的代码执行后,原来的登陆按钮从DOM树中被删掉了,但由于映射中还保存着按钮的引用,所以对应的DOM节点仍然会逗留在内存中,除非明确将其从映射中删除或者等到映射本身被销毁。如果这里使用弱映射,当节点从DOM树中被删除后,垃圾回收程序就会立即释放其内存(假设没有其他地方引用这个对象)
1
2
3
4
5const m = new Map()
const loginButton = document.querySelector('#login')
//给这个节点关联一些元数据
m.set(loginButton,{disable: true})1
2
3
4const wm = new WeakMap()
const loginButton = document.querySelector('#login')
wm.set(loginButton,{disable: true})
第3章 Javascript-类、对象、面向对象编程
3.1 构造函数
特点:名称大写开头
1
2
3
4
5
6
7
8function Foo(name,age){
this.name=name
this.age=age
this.class ='class-1'
//return this //默认有这一行
}
var f= new Foo('zhangsan',20)扩展
1
2
3
4
5
6
7
8
9var a = {} //是 new Object()的语法糖
var a = [] //是 new Array()的语法糖
function Foo(...) //是 var Foo = new Function()的语法糖
//可读性,推荐前面的写法
//instanof判断一个函数是否是一个变量的构造函数
var arr = [];
arr instanceof Array; //true
typeof arr //object typeof是无法判断是否是数组
3.2 原型链
创建对象的几种方法
示例
1
2
3
4
5
6
7
8var o1 = { name: 'o1'}
var o2 = new Object({name : 'o2'})
var M =function(name){this.name = name};
var o3 = new M('o3')
var p = {name : 'o4'}
var o4 = Object.create(p)输出
原型链图
1
2
3
4M.prototype.constructor===M //true
o3.__proto__ === M.prototype //true
M.__proto__ === Function.prototype
o3.__proto__.constructor === M //证明o3是M的实例
new 原理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29//Object.create 原理如下
function object(o){
function F(){}
F.prototype = o
return new F()
}
var new2 = function (func){
var o = Object.create(func.prototype) //第一步 创建空对象,空对象关联构造函数的原型对象
var k = func.call(o); //第二步 执行构造函数
if(typeof k === "object"){
return k
}else{
return o
}
}
funtion M(){
this.name = 'Joe'
}
//验证 效果同 new M()
var o6 = new2(M)
o6 instanceof M //true
o6 instanceof Object //true
o6.__proto__.constrctor === M //true
M.prototype.walk = function(){
console.log("walk")
}
o6.walk() // walk所有引用类型都有一个隐式类型
__proto__
属性,属性值是个普通的对象所有函数,都有一个显示原型 prototype属性
所有引用类型,
__proto__
属性值指向它的构造函数的’prototype’属性当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,它会去它的
__proto__
属性(即它的构造函数的prototype)里去找示例
var obj={};obj.a=100; var arr =[];obj.a=100; function fn(){} fn.a=100; console.log(obj.__proto__); console.log(arr.__proto__); console.log(fn.__proto__); console.log(fn.prototype) console.log(obj.__proto__ === Object.prototype) function Foo(name,age){ this.name=name; } Foo.prototype.alertname=function(){ alert(this.name) } var f =new Foo('zhangsan') f.printName =function(){ console.log(this.name) } f.printName(); f.alertName();//印证最后一个规则 f.toSting();//要去f.__proto__.proto__查找,解析看下图 for(var item in f){ //高版本的浏览器里的for in 已经屏蔽了来自原型的属性 //但是为了代码的健壮性,还是建议加上 if(f.hasOwnProperty(item)){ console.log(item) //循环对象自身的属性,即上面例子的name和printName } }
解析:f是引用类型,引用类型都有一个隐示原型
__proto__
即是它的构造函数Foo的显示原型prototype,构造函数都有一个显示原型,故找不到就会继续往下找,Foo.prototype是一个对象,它也有隐示原型__proto__
,它的构造函数是Object,故f.toString()要去f.__proto__.__proto__
里去找
练习
function Person(name) { this.name = name } let p = new Person('Tom');
p.__proto__
等于什么?Person.__proto
__等于什么?解析:实例的
__proto__
属性(原型)等于其构造函数的prototype属性。实例p的构造函数为Person,而Person的构造函数为Function,结果就一目了然了。问一答案: Person.prototype,问二答案: Function.prototype
封装一个Dom 查询的例子
function Elem(id){ this.elem =document.getElementById(id) } Elem.prototype.html=function(val){ var elem=this.elem; if(val){ elem.innerHTML=val return this//链式操作 }else{ return elem.innerHTML } } Elem.prototype.on = function(type,fn){ var elem=this.elem elem.addEventListener(type,fn) } //使用 var div1=new Elem('test') //console.log(div1.html()) div1.html('<p>hello</p>') div1.on('click',function(){ alert('element') })
3.3 instanceof 原型规则
可以判断引用类型是哪个构造函数的方法
f instanceof Foo
判断逻辑:f会往
__proto__
一层层往上找,指向的是Foo的prototype就返回truef instanceof Object
判断类型
var arr=[] console.log(arr instanceof Array)// true typeof(arr) // object
注意:typeof不能判断数组
typeof区别
- typeof 运算符时采用引用类型存储值会出现一个问题,无论引用的是什么类型的对象,它都返回 “object”
通过代码阐述的内部机制,假设现在有
x instanceof y
一条语句,则其内部实际做了如下判断:while(x.__proto__!==null) { if(x.__proto__===y.prototype) { return true; break; } x.__proto__ = x.__proto__.proto__; } if(x.__proto__== null) { return false; }
x会一直沿着隐式原型链
__proto__
向上查找直到x.__proto__.__proto__......===y.prototype
为止,如果找到则返回true,也就是x为y的一个实例。否则返回false,x不是y的实例。示例一
function F() {} function O() {} O.prototype = new F(); var obj = new O(); console.log(obj instanceof O); // true console.log(obj instanceof F); // true console.log(obj.__proto__ === O.prototype); // true console.log(obj.__proto__.__proto__ === F.prototype); // true
根据new 的内部机制改写上面代码
function F() {} function O() {} var obj = (function () { var obj1 = {}; obj1.__proto__ = F.prototype; // new F(); O.prototype = obj1; // O.prototype = new F(); obj.__proto__ = O.prototype; // new O(); obj.__proto__ = obj1; return obj; })
示例二
调整下代码顺序
function F() {} function O() {} var obj = new O(); O.prototype = new F(); console.log(obj instanceof O); // false console.log(obj instanceof F); // false console.log(obj.__proto__ === O.prototype); // false console.log(obj.__proto__.__proto__ === F.prototype); // false
练习
如果Student inherit from Person(Student类继承Person,需是基于原型的继承),let s = new Student(‘Lily’),那么s instanceof Person返回什么?
function Person (name) { this.name = name; } function Student () { } Student.prototype = Person.prototype; Student.prototype.constructor = Student; let s = new Student('Tom'); console.log(s instanceof Person); // 返回 true
需要注意的是,==如果表达式 obj instanceof Foo 返回true,则并不意味着该表达式会永远返回ture,因为Foo.prototype属性的值有可能会改变,改变之后的值很有可能不存在于obj的原型链上,这时原表达式的值就会成为false。==另外一种情况下,原表达式的值也会改变,就是改变对象obj的原型链的情况,虽然在目前的ES规范中,我们只能读取对象的原型而不能改变它,但借助于非标准的__proto__魔法属性,是可以实现的。比如执行obj.__proto__ = {}之后,obj instanceof Foo就会返回false了。
[toc]
3.4 继承
3.4.1 原型链继承
- 原型链基本思想就是通过原型继承多个引用类型的属性和方法。实现原型链代码模式: SubType通过创建SuperType的实例并将其赋值给自己的原型SubType。prototype实现了对SuperType的继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20function SuperType(){
this.property = true
}
SuperType.prototype.getSuperValue = function () {
return this.property
}
function SubType(){
this.subproperty = false
}
//继承SuperType
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function(){
return this.subproperty
}
let instance = new SubType()
console.log(instance.getSuperValue()) //true![](https://note.youdao.com/yws/api/personal/file/7428abbc8445d51b12c3cab548fca20d?method=download&shareKey=9a24eb888e3717b58c60a1ebcb4e3c74)
**由于SubType.prototype的constructor属性被重写为指向SuperType,所以instance.constructor也指向SuperType**
调用instance.getSuperValue()经过了3步:instance.SubType.prototype和SuperType.prototype,最后一步才找到这个方法,对属性和方法的搜索会一直持续到原型链的末端
默认原型:任何函数的默认原型都是一个Object的实例,这意味着这个实例有一个内部指针指向Object.prototype。这就是为什么自定义类型能够继承toString()、valueOf()在内的所有默认方法的原因
在调用instance.toString()时,实际上调用的是保存在Object.prototype上的方法
判断实例是否在某原型上
- instanceof
1
2
3console.log(instance instanceof Object) //true
console.log(instance instanceof SuperType) //true
console.log(instance instanceof SubType) //true - isPrototypeOf()
1
2
3console.log(Object.prototype.isPrototypeOf(instance)) //true
console.log(SuperType.prototype.isPrototypeOf(instance)) //true
console.log(SubType.prototype.isPrototypeOf(instance)) //true
- instanceof
重写原型链:以对象字面量方式创建原型方法会破环之前的原型链
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24function SuperType(){
this.property = true
}
SuperType.prototype.getSuperValue = function(){
return this.property
}
functionSubType(){
this.subproperty = false
}
//继承SuperType
SubType.prototype = new SuperType()
//通过对象字面量添加新方法,这会导致上一行无效
SubType.prototype = {
getSubValue(){
return this.subproperty
}
someOtherMethod(){
return false
}
}
let instance = new SubType()
console.log(instance.getSuperValue()) //Error子类的原型被一个字面量覆盖了,覆盖后的原型是一个Object实例,而不再是SuperType实例,因此SubType和SuperType之间也没有关系了了
3.4.2 盗用构造函数
- 解决原型包含引用值导致的继承问题,使用call()和apply()方法以新创建的对象为上下文执行构造函数 使用call()(或apply())方法,相当于新的SubType对象上运行了SuperType()函数中所有的初始化代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14function SuperType(){
this.colors = ["red","blue","green"]
}
functin SubType(){
//SuperType
SuperType.call(this)
}
let instance1 = new SubType()
instance1.colors.push("black")
console.log(instance1.colors) //["red","blue","green","black"]
let instance2 = new SubType()
console.log(instance2.colors)//["red","blue","green"] - 传递参数
1
2
3
4
5
6
7
8
9
10
11function SuperType(name){
this.name = name
}
function SubType(){
SuperType.call(this,'Nicholas')
this.age = 29
}
let instance = new SubType()
console.log(instance.name) //"Nicholas"
console.log(instance.age) //29 - 缺点是函数不能重用
3.4.3 组合继承
- 综合了上面原型链和盗用函数,将两者优点集中起来。基本思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29function SuperType(name) {
this.name = name
this.colors = ["red","blue","green"]
}
SuperType.prototype.sayName = function(){
console.log(this.name)
}
function SubType(name, age){
//继承属性
SuperType.call(this,name)
this.age = age
}
//继承方法
SubType.prototype = new SuperType()
SubType.prototype.sayAge = function(){
console.log(this.age)
}
let instance1 = new SubType('Nicholas', 29)
instance1.colors.push('black')
console.log(instance1.colors) //["red","blue","green","black"]
instance1.sayName() //"Nicholas"
console.log(instance1.sayAge()) //29
let instance2 = new SubType("Greg",27)
console.log(instance2.colors) //["red","blue","green"]
instance2.sayName() //"Greg"
instance2.sayAge() //273.4.4 原型式继承
- 不定义自定义类型也可以通过原型实现对象之间的信息共享,ES增加了Object.create()方法规范了这种继承 以上object的过程其实是对传入对象执行了一次浅复制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23//原理
function object(o){
function F(){}
F.prototype = o
return new F()
}
let person ={
name: 'Nicholas',
friends:["Shelby,"Court","Van"]
}
let anotherPerson = object(person)
//当只有一个参数时等价于
//let anotherPerson = Object.create(person)
anotherPerson.name = "Greg"
anotherPerson.friends.push("Rob")
let yetAnotherPerson = object(person)
//当只有一个参数时等价于
//let anotherPerson = Object.create(person)
yetAnotherPerson.name = "Linda"
yetAnotherPerson.frineds.push("Barbie")
console.log(person.friends) //["Shelby","Court","Van","Rob","Barbie"] - 第二个参数与Object.defineProperties()的第二个参数一样:每个新增属性通过各自的描述符来描述
1
2
3
4
5
6let anotherPerson = Object.create(person,{
name:{
value:"Greg"
}
})
console.log(anotherPerson.name) //"Greg" - 原型式继承非常适合不需要单独创建构建函数,但仍然需要在对象间共享信息。
3.4.5 寄生式继承
- 寄生式继承思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象
1
2
3
4
5
6
7function createAnother(original){
let clone = object(original) //通过调用函数创建一个新对象
clone.sayHi = function(){ //增强对象
console.log('hi')
}
return clone //返回这个对象
} - 缺点是这种模式给对象添加函数会导致函数难以重用
3.4.6 寄生式组合继承
寄生式组合继承基本模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24//接收子类构造函数和父类构造函数
function inheritPrototype(subType,superType){
let prototype = object(superType.prototype)
prototype.constructor = subType //增强对象
subType.prototype = prototype //赋值对象
}
function SuperType(name){
this.name = name
this.colors = ["red", "blue", "green"]
}
SuperType.prototype.sayName = function (){
console.log(this.name)
}
function SubType(name,age){
SuperType.call(this.name)
this.age = age
}
inheritPrototype(subType,SuperType)
SubType.prototype.sayAge = function(){
console.log(this.age)
}此方法是引用类型继承的最佳模式
3.5 类
3.5.1 类定义
- 类定义
1
2
3
4//类声明
class Person{}
//类表达式
const Animal = class{} - 函数可以提升,但类定义不行
1
2
3
4
5
6
7
8
9
10
11//函数提升
console.log(FunctionDeclaration) // FunctionDeclaration(){}
function FunctionDeclaration (){}
//类表达式
console.log(ClassExpression) //undefined
var ClassExpression = class {}
//类声明
console.log(ClassDeclaration) //ReferenceError
class ClassDecalaration{} - 函数受函数作用域限制,类受块作用域限制
1
2
3
4
5
6{
function FunctionDeclaration(){}
class ClassDeclaration {}
}
console.log(FunctionDeclaration) //FunctionDeclaration(){}
console.log(ClassDeclaration) //ReferenceError - 类表达式名称是可选的,把类表达式赋值给变量后,可通过name属性取得类表达式的名称字符串,但不能在类表达式作用域外部访问这个标识符
1
2
3
4
5
6
7
8
9
10let Person = class PersonName{
identify(){
console.log(Person.name,PersonName.name)
}
}
let p = new Person()
p.identify() //PersonName PersonName
console.log(Person.name) //PersonName
console.log(PersonName) //Reference3.5.2 类构造函数
- constructor关键字用于在类定义块内部创建类构造函数。它会告诉解析器在使用new操作符创建类实例时,应该调用该函数。构造函数不是必须的,不定义相当于空函数。
- 类实例化时传入的参数会作构造函数的参数。如果不传参,类名后括号可选
1
let p1 = new Person
- 类构造函数返回其他对象时,这个对象不会通过instanceOf操作符检测出与类有关联,因为这个对象原型指针没有被改变
1
2
3
4
5
6
7
8
9
10
11
12
13class Person{
constructor(override){
this.foo = 'foo'
if(override){
return {
bar: 'bar'
}
}
}
}
let p = new Person(true)
console.log(p) //{bar:'bar'}
console.log(p instanceOf Person) //false3.5.3 实例、原型和类成员
- 每个实例都对应一个唯一的成员对象,这意味着所有成员都不会在原型上共享
- 为了在实例间共享方法,类块中定义的方法作为原型方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43const symbolKey = Symbol('symbolKey')
class Person{
constructor(){
//添加到this的所有内容都存在于不同的实例上
this.locate = () => console.log('instance')
}
//在类块中定义的所有内容都会定义在类原型上,方便共享
locate(){
console.log('prototype',this)
}
//不能在类块中给原型添加原始值或对象作为成员数据
//name: 'Jake' //Uncaught
//类方法等同于对象属性,因此可以使用字符串、符号或者计算值作为键
[symbolKey](){
console.log(....)
}
['conputed'+ 'Key'](){
console.log(...)
}
//类定义也支持获取和设置访问器
set name(newNames){
this.name_ = newNames
}
get name(){
return this.name_
}
//静态成员每个类上只能有一个
static locate(){
console.log('class', this)
}
}
let p = new Person()
p.locate() //instance, Person{}
Person.prototype.locate() //'prototype',{constructor:...}
Person.locate() //class, class Person{}
p.name = "Jake"
console.log(p.name) //'Jake' - 静态类方法非常适合作为实例工厂
1
2
3
4
5
6
7
8
9
10
11
12
13
14class Person{
constructor(age){
this.age_ = age
}
sayAge(){
console.log(this.age_)
}
static create(){
//使用随机年龄创建并返回一个Person实例
return new Person(Math.floor(Math.random()*100))
}
}
console.log(Person.create()) //Person{age_:...} - 类定义语法支持在原型和类本身定义生成器方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35class Person {
constructor(){
this.nicknames = ['Jack','Jake','J-Dog']
}
//类原型上
*createNicknameIterator(){
yield 'Jack';
yield 'Jake';
yield 'J-Dog';
}
//在类上
static *createJobIterator(){
yield 'Butcher';
yield 'Baker';
yield 'Candlestick maker'
}
//因为支持生成器方法,可以添加一个默认的迭代器,把类实例变成可迭代对象
*[Symbol.iterator](){
yield *this.nickname.entries()
}
//也可以只返回对象实例 效果一样
//[Symbol.iterator](){
// return this.nicknames.entries()
//}
}
let p = new Person()
for(let [idx, nickname] of p){
console.log(nickname)
}
//Jack
//Jake
//J-Dog3.5.4 类继承
- 类继承类,也可以继承普通的构造函数 派生类会通过原型链访问到类和原型上定义的方法,this的值会反映调用相应方法的实例或类
1
2
3
4
5
6
7
8
9
10
11
12
13class Vehicle{}
//继承类
class Bus extends Vehicle {}
let b = new Bus()
console.log(b instanceOf Bus) //true
console.log(b instanceOf Vehicle) //true
function Person(){}
//继承普通构造函数
class Engineer extends Person{} - 派生类在调用super()后才能使用this,super方法仅限于派生类的构造函数和静态方法内部使用。调用super()会调用父类构造函数,并将返回的实例赋值给this。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23class Vehicle{
constructor(){
this.hasEngine = true
}
static identify(){
console.log('vehicle')
}
}
class Bus extends Vehicle {
constructor(){
//不要在调用super()之前引用this
super()
console.log(this) //Bus {hasEngine :true}
}
//在静态方法中可以通过super调用继承在类上定义的静态方法
static identify(){
super.identify()
}
}
new Bus()
Bus.identify() //'vehicle' - 如果没有定义类构造函数,在实例化派生类时会调用super(),而且会传入所有传给派生类的参数
- 如果在派生类中显式定义了构造函数,则必须在其中调用super(),或者在其中返回一个对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Vehicle{}
class Car extends Vehicle {}
class Bus extends Vehicle {
constructor(){
super()
}
}
class Van extends Vehicle{
constructor(){
return {}
}
}
console.log(new Car()) //Car{}
console.log(new Bus()) //Bus{}
console.log(new Van()) //{} - 抽象基类,供其他类继承,本身不会被实例化。通过实例化时检测new.target是不是抽象基类,可以阻止对抽象基类的实例化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class Vehicle{
constructor(){
console.log(new.target)
if(new.target === Vehicle){
throw new Error('Vehicle cannot be directly instantiated')
}
//要求派生类必须定义foo方法
if(!this.foo){
throw new Error('Inheriting class must define foo()')
}
}
}
//派生类
class Bus extends Vehicle{
foo(){}
}
//派生类
class Van extends Vehicle{}
new Bus() //class Bus{}
new Vehicle() //'VEhicle cannot be derectly instantiated'
new Van() //Error: Inheriting class must define foo() - 继承内置类型
1
2
3class SuperArray extends Array{
....
}
第4章 Javascript-函数
4.1 箭头函数
- 箭头函数不能使用arguments、super和new.target,也不能用作构造函数,也没有prototype属性
1
2
3
4
5
6
7let arrowSum = (a, b) => {
return a + b;
}
//嵌入函数场景
let ints = [1, 2, 3]
insts.map((i) => i + 1 )4.2 函数名
- 函数名就是指向函数的指针,这意味这一个函数可以有多个名称
1
2
3
4
5
6
7
8
9
10
11
12
13
14function sum(num1, num2){
return num1 + num2
}
console.log(sum(10, 10)) //20
//与sum指向同一个函数
let anotherSum = sum
console.log(anotherSum(10, 10)) //20
//切断与函数之间的关联
sum = null
//照常可以使用
console.log(anotherSum(10, 10)) //20 - 所有函数对象都会暴露一个只读name属性,如果用Function构造函数创建,则会标识成”anonymous”
1
2
3
4
5
6
7
8
9
10function foo(){}
let bar = function(){}
let baz = () => {}
console.log(foo.name) //foo
console.log(bar.name) //bar
console.log(baz.name) //baz
console.log((()=> {}).name) //(空字符串)
console.log((new Function()).name) //anonymous
console.log((foo.bind(null)).name) //bound foo4.3 理解参数
- 函数的参数内部表现为一个数组,函数被调用时总会接收一个数组
- 在使用function关键字定义(非箭头)函数时,可以在函数内部访问arguments对象,从中获取传进来的每个参数值。arguments对象是一个类数组对象(但不是Array实例),通过arguments[index]访问。通过arguments[0]取得同样的参数值,把函数重写成不声明参数也可以 函数参数只是为了方便才写出来,并不是必须写出来
1
2
3
4
5
6
7function sayHi(name,message) {
console.log("Hello " + name + '.' + message)
}
//等价于
function sayHi(){
console.log("Hello "+ arguments[0] + '.' + arguments[1])
} - 通过
arguments.length
属性检查传入的参数个数,注意它的值不是定义函数时命名参数的个数。1
2
3
4
5
6function howManyArgs(){
console.log(arguments.length)
}
howManyArgs("string", 45) //2
howManyArgs(1) //1
howManyArgs() //0 - arguments对象的值会自动同步对应命名参数,但这不意味着它们都访问同一个内存地址,它们还是分开的,只是保持同步而已。严格模式下则要注意,给arguments重新赋值也不会影响命名参数传入的值。
- 所有参数都是按值传递,不可能按引用传递。如果是对象则传递的值就是这个对象的引用。
4.4 默认参数值
- 显式定义默认参数
1
2
3
4
5
6
7
8function makeKing(name = 'Herry'){
...
}
//不局限于原始值或对象值,还可以是调用函数返回值
function makeLing(name = getNumerais()){
...
}
函数默认参数只有在函数调用时才会求值,而且未传参的情况下才会被调用 - 因为参数时按顺序初始化的,所以后定义默认的参数可以引用先定义的参数
1
2
3function makeKing(name = "Herry", numerals = name){
...
}4.5 参数扩展与收集
- 使用扩展操作符 对arguments对象而言,它并不知道扩展操作符的存在,而是按照函数传入的参数接收每个值
1
2
3
4
5
6
7
8
9
10
11
12let values = [1, 2, 3, 4]
console.log(getSum(-1,...values)) /9
console.log(getSum(...values,5)) //15
console.log(getSum(-1,...values,5)) //14
console.log(getSum(...values,...[5, 6, 7])) //28
//同时可以用于命名参数,也可以使用默认参数
function getProduct(a, b, c = 1){
return a * b * c
}
console.log(getProduct(...[1,2])) //2
console.log(getProduct(...[1,2,3])) //6 - 函数的收集参数, 只能作为最后一个参数使用
1
2
3
4
5
6
7
8
9
10//error
function getProduct(...values, lastValue){}
function ignoreFirst(firstValue, ...values){
console.log(values)
}
ignoreFirst() //[]
ignoreFirst([1]) //[]
ignoreFirst([1,2]) //[2]
4.6 函数声明与函数表达式
- 函数声明与函数表达式区别在于:函数声明会变量提升
1
2
3
4
5
6
7
8
9
10
11
12//没问题
console.log(sum(10,10))
function sum(num1, num2){
return num1 + num2
}
//会出错,这并不是let导致的,var关键字也会碰到同样问题
console.log(sum(10, 10))
let sum = function(num1, num2){
return num1 + num2
}
4.7 函数内部
arguments
- arguments包含调用函数传入的所有参数,这个对象只有以function关键字定义函数时才有
- arguments.callee作用是调用相同函数名 上面的函数正确执行必须保证函数名是factorial,从而导致了紧密耦合,使用arguments.callee就可以让函数逻辑与函数名解耦
1
2
3
4
5
6
7function factorial(num){
if(num <= 1){
return 1
}else{
return num * factorial(num - 1)
}
}这意味着无论函数叫什么名称,都可以引用正确的函数1
2
3
4
5
6
7function factorial(num){
if(num <= 1){
return 1
}else{
return num * arguments.callee(num -1)
}
}
this
- 在标准函数中,this引用的是把函数当成方法调用的上下文对象。(在网页的全局上下文中调用函数时,this指向windows)
1
2
3
4
5
6
7
8
9
10
11window.color = 'red'
let o ={
color: 'blue'
}
function sayColor(){
console.log(this.color)
}
sayColor() //"red"
o.sayColor = sayColor
o.sayColor() //"blue" - 在箭头函数中,this会保留定义该函数时的上下文 在事件回调或定时回调中调用某个函数时
1
2
3
4
5
6
7
8
9window.color = "red"
let o = {
color: "blue"
}
let sayColor = () => console.log(this.color)
sayColor() //"red"
o.sayColor = sayColor
o.sayColor() //"red"1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function King(){
this.royaItyName = "Herry"
//this引用King的实例
setTimeout(()=> console.log(this.royaItyName),1000)
}
function Queen(){
this.royaItyName = "Elizabeth"
//this引用window对象
setTimeout(function(){
console.log(this.royaItyName)
},1000)
}
new King() //"Herry"
new Queen() //undefined - 闭包中使用this会让代码变得复杂 由于每个函数被调用时会自动创建两个特殊变量:this和arguments。内部函数永远不可能直接访问外部函数的这两个变量。但是,如果把this保存到闭包可用访问的另一变量中:
1
2
3
4
5
6
7
8
9
10window.identity = 'The Window'
let object = {
identity : 'My object',
getIdentityFunc(){
return function(){
return this.identity
}
}
}
console.log(object.getIdentityFunc()()) //'Tne Window'1
2
3
4
5
6
7
8
9
10
11window.identity = 'The Window'
let object = {
identity:'My Object',
getIdentityFunc(){
let that = this
return function(){
return this.identity
}
}
}
console.log(object.getIdentityFunc()()) //'My Object'caller
- caller 调用当前函数的函数 以上代码会显示outer()函数的源代码,这是因为outer()调用了inner(),inner.caller指向outer,如果要降低耦合度,可通过arguments.callee.caller来引用同样的值。注意在严格模式下访问argument.callee会报错。
1
2
3
4
5
6
7function outer(){
inner()
}
function inner(){
console.log(inner.caller)
}
outer()
4.8 函数属性与方法
length
- length属性保存函数定义的命名参数的个数
1
2
3
4
5
6
7
8function sayName(name){
console.log(name)
}
function sun(num1, num2){
return num1 + num2
}
console.log(sayName.length) //1
console.log(sum.length) //2apply、call与bind
- 这两个方法都会以指定的this值来调用函数
- apply方法接收两个参数:函数内this的值和一个参数数组,第二个参数可以是Array实例,也可以是arguments对象 以上的this指向的是window。注意严格模式下,调用函数如果没有指定上下文对象,this不会指向window,除非使用apply()或call()把函数指定给一个对象,否则this会变成undefined
1
2
3
4
5
6
7
8
9
10
11
12
13function sum(num1, num2){
return num1 + num2
}
//传入arguments
function callSum1(num1, num2){
return sum.apply(this,arguments)
}
//传入数组
function callSum2(num1, num2){
return sum.apply(this, [num1, num2])
}
console.log(callSum1(10 ,10)) //20
console.lo(callSum2(10, 10)) //20 - call区别在于第二个参数开始,要传递给被调用函数的参数是逐个传递的,必须把参数一个个列出来
1
2
3
4
5
6function sum(num1, num2){
return num1 + num2
}
function callSum(num1, num2){
return sum.call(this.num1, num2)
} - apply和call强大的作用是控制函数调用上下文即函数体内this的能力 可以将任意对象设置为任意函数的作用域,这样对象可以不用关系方法
1
2
3
4
5
6
7
8
9
10
11
12
13window.color = "red"
let o = {
color:"blue"
}
function sayColor(){
console.log(this.color)
}
sayColor() //"red"
sayColor.call(this) //"red"
sayColor.call(window) //"red"
//把函数执行上下文即this切换为对象o
sayColor.call(o) //"blue" - ES5出于同样的目的,定义了bind()方法,它会创建一个新的函数实例,其this值会被绑定到传给bind()的对象
1
2
3
4
5
6
7
8
9window.color = "red"
var o ={
color:"blue"
}
function sayColor(){
console.log(this.color)
}
let objectSayColor = sayColor.bind(o)
objectSayColor() "blue"
4.9 递归
- 在非严格模式下,使用arguments.callee是引用当前函数的首选
- 严格模式下不能访问arguments.callee,可用命名函数表达式:
1
2
3
4
5
6
7
8
9const factorial = (
function f(num){
if(num <= 1){
return 1
}else{
return num * f(num -1)
}
}
)
4.10 内存泄漏
- 只要匿名函数存在,element的引用计数就至少等于1,内存就不会被回收 优化:
1
2
3
4function assignHandler(){
let element = document.getElementById('someElement')
element.onclick = () => console.log(element.id)
}闭包改为引用一个保存着element.id的变量id,从而消除了循环引用,但即使闭包没有直接引用element,包含函数的活动对象上还是保存着对它的引用,因此必须把element设置为null。1
2
3
4
5
6function assignHandler(){
let element = document.getElementById('someElement')
let id = element.id
element.onclick =() => console.log(id)
element = null
}4.11 私有变量
特权方法
- 可用访问私有变量的公共方法叫做特权方法
- 构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14function MyObject(){
//私有变量和私有函数
let privateVariable = 10
function privateFunction(){
return false
}
//特权方法:其实是个闭包
this.publicMethod = function(){
privateVariable++;
return privateFunction()
}
} - 原型模式 像这样创建静态私有变量可用利用原型更好的重用代码,但是每个实例共享了这个私有变量,一个实例修改了变量会影响另一个实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22(function(){
let name = ''
Person = function(value){
name = value
}
Person.prototype.getName = function(){
return name
}
Person.prototype.setName = function(value){
name = value
}
}
)()
let person1 = new Person('Nicholas')
console.log(person1.getName()) //'Nicholas'
person1.setName('Matt')
console.log(person1.getName()) //'Matt'
let person2 = new Person('Michael')
console.log(person1.getBAme()) 'Michael'模块模式
- 构造函数
- 单例对象实现隔离和封装,单例对象就是只有一个实例的对象。在Web开发中,经常需要使用单例对象管理应用程序级的信息,下面创建了一个application对象管理组件: 在模块模式中,单例对象作为一个模块,经过初始化包含某些私有数据,而这些数据又可用通过暴露的公共方法来访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21let application = function(){
//私有变量和私有函数
let components = new Array()
//初始化
components.push(new BaseComponent())
//公共接口
return {
//返回注册组件的数量
getComponentCount(){
return components.length
}
//注册新组件
registerComponent(component){
if(typeof component == 'object'){
components.push(component)
}
}
}
} - 如果上一节的application对象必须是BaseComponent实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23let application = function(){
//私有变量和私有函数
let component = new Array()
//初始化
component.push(new BaseComponent())
//创建局部变量保存实例
let app = new BaseComponent()
//公共接口
app.getComponent = function (){
return components.length
}
app.registerComponent = function(component){
if(typeof component == 'object'){
components.push(component)
}
}
return app
}()
第5章 Javascript-异步
5.1 Promise
5.1.1 Promise基础
- Promise通过new操作符来实例化,期约是有个有状态的对象,可能处于如下3种状态之一:
- 待定(pending),最初的状态
- 兑现(fulfilled,有时也称为resolved)
- 拒绝(rejected)
无论落定是哪种状态,只要从待定转换为兑现或拒绝,期约的状态就不再改变。期约将异步封装起来,从而隔离外部的同步代码。
- 调用resolve()会将状态切换为兑现,调用reject()会将状态切换为拒绝
- Promise.resolve
1
2
3let p1 = new Promise((resolve, reject) => resolve())
//等同于
let p2 = Promise.resolve() - Promise.prototype.catch() 用于给期约添加拒绝处理程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24let p = Promise.reject()
let onRejected = function(e){
setTimeout(console.log,0, 'rejected')
//这两种添加拒绝处理程序是一样的
p.then(null, onRejected) //rejected
//p.catch(onRejected)
- Promise.prototype.finally()处理程序没有办法知道期约的状态是解决还是拒绝,这个方法主要用于清理代码
- 消息队列
- 示例
```js
//创建解决期约
let p = Promise.resolve()
//添加解决处理程序
p.then(()=> console.log('onResolved handler'))
//同步输出,证明then()已经返回
console.log('then() return')
//实际输出
//then() return
//onResolved handler在一个解决期约上调用then()会把onResolved处理程序推进消息队列,但这个处理程序在当前线程上的同步代码执行前不会执行
- 示例二
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20let synchronousResolve
//创建一个期约并将解决函数保存在一个局部变量中
let p = new Promise((resolve) =>{
synchronousResolve = function(){
console.log('1:invokong resolve()''=)
resolve()
console.log('2: resolve() returns')
}
})
p.then(() => console.log('4: then() handler executes'))
synchronousResolve()
console.log('3: synchronousResolve() returns')
//实际输出
//1: invoking resolve()
//2: resolve() returns
//3: synchronousResolve() returns
//4: then() handler exectues - 示例三
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25let p1 = Promise.resolve()
p1.then(()=>console.log(‘p1.then() onResolved’))
console.log(‘p1.then() returns’)
let p2 = Promise.reject()
p2.then(()=>console.log(‘p2.then() onRejected’))
console.log(‘p2.then() returns’)
let p3 = Promise.reject()
p3.then(()=>console.log(‘p3.then() onRejected’))
console.log(‘p3.catch() returns’)
let p4 = Promise.resolve()
p4.finally(()=>console.log(‘p4.finally() onFinally’))
console.log(‘p4.finally() returns’)
//p1.then() returns
//p2.then() returns
//p3.catch() returns
//p4.finally() returns
//p1.then() onResolved
//p2.then() onRejected
//p3.catch() onRejected
//p4.finally() onFinally
- 示例二
- 拒绝期约与拒绝错误处理
- 顺序 追寻错误信息如下:
1
2
3
4
5
6
7
8
9
10
11let p1 = new Promise((resolve,reject) => reject(Error('foo')))
let p2 = new Promise((resolve,reject) => {
throw Error('foo')
})
let p3 = Promise.resolve().then(()=> { throw Error('foo')})
let p4 = Promise.reject(Error('foo'))
setTimeout(console.log,0,p1)// Promise <rejected>: Error: foo
setTimeout(console.log,0,p2) //Promise <rejected> Error: foo
setTimeout(console.log,0,p3) //Promise <rejected> Error: foo
setTimeout(console.log,0,p4) //Promise <rejected> Error: fooPromise.resolve().then()的错误最后才出现,这是因为它需要在运行时消息队列中添加处理程序,也就是说,在最终抛出未捕获错误之前它还会创建另一个期约1
2
3
4
5
6
7
8...
at test.html:5
...
at test.html:6
...
at test.html:8
...
at test.html:7 - 正常情况下,在通过throw()关键字抛出错误时,Javascript运行时的错误处理机制会停止执行抛出错误之后的任何指令,但在期约中抛出错误时,因为错误实际上是从消息队列中异步抛出的,所以并不会阻止运行时继续执行同步指令 异步错误只能通过异步的onRejected处理程序捕获
1
2
3
4
5
6
7
8
9
10
11throw Error('foo')
console.log('bar') //这行不会执行
//Uncaught Error: foo
Promise.reject(Error('foo'))
console.log('bar')
//bar
//Uncaught (in promise) Error: foo上面不包括捕获执行函数中的错误,在解决或拒绝期约之前,仍然可以使用try/catch在执行函数中捕获错误:1
2
3
4
5
6
7//正确
Promise.reject(Error('foo')).catch((e) => {})
//不正确
try{
Promise.reject(Error('foo'))
}catch(e){}1
2
3
4
5
6
7
8
9let p = new Promise((resolve,reject) =>{
try{
throw Error('foo')
}catch(e){}
resolve('bar')
})
setTimeout(console.log)
- 顺序
5.1.2 Promise.all
- Promise.all()静态方法创建的期约会在一组期约全部解决之后再解决,这个静态方法接收一个可迭代对象,返回一个新期约 如果包含一个待定/拒绝期约,期约也会待定/拒绝
1
2
3
4
5
6
7
8
9
10
11
12
13
14let p1 = Promise.all([
Promise.resolve()
Promise.resolve()
])
//可迭代对象元素会通过Promise.resolve()转换为期约
let p2 = Promise.all([3, 4])
//空的可迭代对象等价于Promise.resolve()
let p3 = Promise.all([])
//无效语法
let p4 = Promise.all()
//TypeError如果所有期约都成功被解决,则会按迭代顺序:1
2
3
4
5
6
7
8
9
10
11//永远待定
let p1 = Promise.all(new Promise(()=> {}))
setTimeout(console.log,0, p1) //Promise <pending>
//一次拒绝会导致最终期约拒绝
let p2 = Promise.all([
Promise.resolve()
Promise.reject()
Promise.resolve()
])
setTimeout(console.log,0,p2) //Promise <rejected>如果有期约拒绝,则第一个拒绝的期约会将自己作为拒绝理由,之后再拒绝的期约不会影响最后的拒绝结果,但并不影响之后期约正常的拒绝操作,仍然会处理,不会有错误被漏掉1
2
3
4
5
6let p = Promise.all([
Promise.resolve(3)
Promise.resolve()
Promise.resolve(4)
])
p.then((values) => setTimeout(console.log,0, values)) //[3,undefined,4]1
2
3
4
5
6let p = Promise.all([
Promise.reject(),
new Promise((resolve,reject) => setTimeout(reject,1000)
])
p.catch((reason) => setTimeout(console.log,0,reason)) //3
//没有未处理的错误5.1.3 Promise.race
- Promise.race()静态方法返回一个包装期约,是一组集合中最先解决或拒绝的期约镜像 无论是拒绝/解决期约,它都会包装其解决值或拒绝理由返回新期约
1
2
3
4
5
6
7
8
9
10
11
12
13let p1 = Promise.race([
Promise.resolve(),
Promise.resolve()
])
//可迭代对象中的元素会通过Promise.resolve()转换为期约
let p2 = Promise.race([3,4])
//空的可迭代对象等价于new Promise(() => {})
let p3 = Promise.race([])
//无效
let p4 = Promise.race()
//TypeError1
2
3
4
5
6//解决先发生,超时后的拒绝被忽略
let p1 = Promise.race([
Promise.resolve(3),
new Promise((resolve,reject) => setTimeout(reject, 1000))
])
setTimeout(console.log,0,p1) //Promise <resolved>: 35.2 Async 异步函数
5.2.1 基础
- async关键字用于声明异步函数
1
2
3
4
5
6async function foo(){}
let bar = async function(){}
let baz = async ()=>{}
class Qux{
async qux(){}
} - 异步函数始终返回期约对象,在函数外部调用这个函数可以返回它返回的期约
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15async function foo(){
console.log(1)
return 3
//这里返回一个期约对象也是一样的
//return Promise.resolve(3)
}
//给返回的期约添加一个解决处理程序
foo().then(console.log)
console.log(2)
//1
//2
//35.2.2 await
- await 用于暂停和恢复执行
- 示例一
1
2
3
4
5
6
7
8
9let p = new Promise((resolve,reject) => setTimeout(resolve,1000,3))
p.then(x => console.log(x)) //3
//使用async/await 可以写成这样
async function foo(){
let p = new Promise((resovle,reject) => setTimeout(resolve,1000,3))
console.log(await p)
}
foo() //3 - 示例二
1
2
3
4
5
6
7
8
9
10
11
12
13async function foo(){
console.log(1)
await Promise.reject(3)
console.log(4) //这行不会执行
}
//给返回的期约添加一个拒绝处理程序
foo().catch(console.log)
console.log(2)
//1
//2
//3 - 示例三
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25async function foo(){
console.log(2)
console.log(await Promise.resolve(8))
console.log(9)
}
async function bar(){
console.log(4)
console.log(await 6)
console.log(7)
}
console.log(1)
foo()
console.log(3)
bar()
console.log(5)
//1
//2
//3
//4
//5
//8
//9
//6
//7
- 示例一
5.2.3 axios
基本用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41const axios = require('axios');
// Make a request for a user with a given ID
axios.get('/user?ID=12345')
.then(function (response) {
// handle success
console.log(response);
})
.catch(function (error) {
// handle error
console.log(error);
})
.then(function () {
// always executed
});
// Optionally the request above could also be done as
axios.get('/user', {
params: {
ID: 12345
}
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
})
.then(function () {
// always executed
});
// Want to use async/await? Add the `async` keyword to your outer function/method.
async function getUser() {
try {
const response = await axios.get('/user?ID=12345');
console.log(response);
} catch (error) {
console.error(error);
}
}API
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// Send a POST request
axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
});
// GET request for remote image in node.js
axios({
method: 'get',
url: 'http://bit.ly/2mTM3nY',
responseType: 'stream'
})
.then(function (response) {
response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'))
});browser 使用
URLSearchParams
1
2
3
4const params = new URLSearchParams();
params.append('param1', 'value1');
params.append('param2', 'value2');
axios.post('/foo', params);
5.2.4 异步函数策略
sleep
1 | async function sleep(delay){ |
5.2.5 串行执行期约
- await直接传递每个函数的返回值,结果通过迭代产生
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function addTwo(x) {
return x + 2
}
function addTree(x){
return x + 3
}
function addFive(x){
return x + 5
}
async function addTen(x){
for(const fn of [addTwo,addTree,addFive]){
x = await fn(x)
}
return x
}
addTen(9).then(console.log) //19
5.3 Promise、Generator、Async三者的区别
Promise
Promise有三种状态:pending(进行中)、resolved(成功)、rejected(失败)
Promise对象的缺点:
1、无法取消Promise,一旦新建它就会立即执行,无法中途取消。
2、如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
3、当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
4、Promise 真正执行回调的时候,定义 Promise 那部分实际上已经走完了,所以 Promise 的报错堆栈上下文不太友好。
Generator
Generator 是ES6引入的新语法,Generator是一个可以暂停和继续执行的函数。
简单的用法,可以当做一个Iterator来用,进行一些遍历操作。复杂一些的用法,他可以在内部保存一些状态,成为一个状态机。
Generator 基本语法包含两部分:函数名前要加一个星号;函数内部用 yield 关键字返回值。
yield表达式本身没有返回值,或者说总是返回undefined。
next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function * foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }
Async
- Async 是 Generator 的一个语法糖。
- async 对应的是 * 。
- await 对应的是 yield 。
- async/await 自动进行了 Generator 的流程控制。
第6章 Javascript-BOM
6.1 window对象
浏览器视口
- 所有现代浏览器都支持4个属性:innerWidth、innerHeight、outerWidth和outerHeight。其中outerWidth和outerHeight返回浏览器窗口自身大小,innerWidth和innerHeight返回浏览器窗口页面视口大小。
- 确定页面视口的大小
1
2
3
4
5
6
7
8
9
10
11
12
13let pageWidth = window.innerWidth,
pageHeight = window.innerHeight
//判断是不是一个数值
if(typeof pageWidth != "number"){
//检查页面是否处于标准模式
if(document.compatMode == "CSSlCompat"){
pageWidth = document.documentElement.clientWidth
pageHeight = document.documentElement.clientHeight
} else{
pageWidth = document.body.clientWidth
pageHeight = document.body.clientHeight
}
} - 常用接口
- window.resizeTo 宽高缩放到新的值
- window.resizeBy 宽高各缩放多少倍
- window.scrollBy 滚动到新坐标,以左上角为0坐标,向下和向右为正
- window.moveTo 移动
- window.close 关闭
导航和打开新窗口
- window.open 如果有一个窗口名叫”toFrame”,则这个窗口会打开,否则会打开一个新窗口并命名为”toFrame”,第二个参数也可以是特殊的窗口名比如_self、_parent、_top或_blank。第三个参数即特性字符串,以一个逗号分隔的设置字符串,可控制窗口的宽高、位置等。
1
window.open("http://www.wrox.com/","topFrame")
这代码会打开一个可缩放新窗口,大小为400*400,位于离屏幕左边及顶边各10像素的位置1
window.open("http://www.wrox.com/","toFrame","height=400,width=400,top=10,left=10,resizable=yes")
定时器
- setTimeout()用于指定在一定时间后执行某些代码,setInterval()用于指定每隔一段时间执行某些代码
- 调用setTimeout时,会返回一个表示该超时的数值ID,用于取消该任务,可以调用clearTimeout()方法并传入超时ID 在指定时间内调用清除函数可以取消任务,否则无效
1
2let timeout = setTimeout(()=> alert('Hello'),1000)
clearTimeout(timeout) - setIntervalue()在实践上很少会在生产环境下使用,因为没办法保证一个任务结束和另一个任务开始之间的间隔
系统对话
- confirm()确定框跟警告框类似,有取消和确定两个按钮
- prompt()提示用户输入信息的弹出框,可以向用户显示信息、确认操作和获取输入
6.2 location对象获取页面信息
解析url地址
解析查询字符串
- 使用下面的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21let getQueryStringArgs = function(){
//取得问好之后的查询字符串
let qs = location.search.length > 0 ? location.search.substring[1] : ""
let args ={}
//把每个参数添加到args对象中
for(let item of qs.split("&").map(kv => kv.split("="))){
let name = decodeURICompoment(item[0]),
value = decodeURIComponent(item[1])
if(name.length){
args[name] = value
}
}
return args
}
//假设查询字符串为?q=javascript&num=10
let args = getQueryStringArgs()
console.log(args["q"]) //"javascript"
console.log(args["num"]) //"10" - URLSearchParams 提供一组标准API方法,通过它们可以检查和修改查询字符串
- 使用下面的函数
通过修改location对象修改浏览器的地址
1
2
3
4//三种修改等效
location.assign("http://www.wrom.com")
window.location = "http://www.wrox.com"
location.href = "http://www.wrox.com" //最常用除了hash之外,修改location的其他属性,也会导致页面重新加载新的url
![](https://note.youdao.com/yws/api/personal/file/5f65ca3dd3ab735288268bb26fef113e?method=download&shareKey=bf9788251bbdf97d56ac2cd75a98543a)
上面的方法修改url之后,浏览器历史记录会新增,如果不想新增历史记录,可以使用
replace()
,但用户不能退回之前的页面。reload()重新加载当前页面,如果页面自上次请求以来没有修改过,浏览器可能从缓存中加载页面,传入true参数则从服务器加载
1
2location.reload() //重新加载,可能从缓存加载
location.reload(true) //重新加载,从服务器加载脚本位于reload()之后的代码可能因为网络延迟等原因不执行,所有最好把reload()作为最后一行代码
6.3 navigation对象通常用来确定浏览器类型
6.4 history对象操作浏览器历史
- history对象表示当前窗口首次使用以来用户的导航历史记录 通常用来创建”前进”和”后退”按钮,以及确定页面是不是用户历史记录中的第一条记录
1
2
3
4
5
6
7
8
9
10//后退一页
history.go(-1)
history.back()
//前进一页
history.go(1)
history.forward()
if(history.length == 1){
//这是用户窗口的第一个页面
}
第7章 Javascript-DOM
7.1 Dom 节点操作
获取Dom节点
var div1=document.getElementById('div1') //元素 var divList=document.getElementsByTagName('div') //集合 var containerList= document.getElementsByClassName('.container') //集合 var pList = document.querySelectorAll('p') //集合
property(js对象属性的修改) 更多细节
var pList = document.querySelectorAll('p') //集合 var p=pList[0] console.log(p.nodeName) //p p.style.width='100px' //获取修改样式
attribute (html标签属性的修改)
var pList = document.querySelectorAll('p') //集合 var p=pList[0] p.getAttribute('data-name') //获取属性 p.setAttribute('data-name','immoc') //自定义属性
新增节点、添加节点
var div1=document.getElementById('div1') var p1 =document.createElement('p') p1.innerHTML='this is p1' div1.appendChild(p1) //移动已有的节点 var p2 =document.getElementById('p2') div1.appendChild(p2)
获取父元素和子元素,删除节点
var div1=document.getElementById('div1') var parent = div1.parentElement var child=div1.childNodes div1.removeChild(child[0])
7.2 Dom 事件模型
Dom 事件模型 : 捕获(从上到下)- 冒泡(从下到上)
事件流即是捕获-目标阶段-冒泡
事件捕获的具体流程window - document - html - body-…
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36<div id="ev">
<style>
#ev{
width: 100px;
height: 100px;
background: red;
color:#fff;
text-align: center;
line-height: 100px;
}
</style>
目标元素
</div>
<script type="text/javascript">
var ev= document.getElementById('ev')
//第三个参数为true,捕获阶段触发
window.addEventListener('click',function(){
console.log('window captrue')
},true)
document.addEventListener('click',function(){
console.log('document captrue')
},true)
document.documentElement.addEventListener('click',function(){
console.log('html captrue')
},true)
document.body.addEventListener('click',function(){
console.log('body captrue')
},true)
ev.addEventListener('click',function(){
console.log('ev captrue')
},true)
</script>输出:
7.3 Dom 事件类
event.preventDefault(a标签阻止跳转行为)
event.stopPropagation(阻止冒泡)
event.stopImmediatePropagation()
event.currentTarget
event.target
自定义事件
Event
<!--Event--> var eve = new Event('custome') ev.addEventListener('custome',function(){ console.log('custome') }); ev.dispatchEvent(eve);
CustomEvent
<!--CustomEvent--> // add an appropriate event listener obj.addEventListener("cat", function(e) { process(e.detail) }); // create and dispatch the event var event = new CustomEvent("cat", { detail: { hazcheeseburger: true } }); obj.dispatchEvent(event);
第8章 Javascript-生成器
7.1 简介
- 生成器是ES6新增的较为灵活的结构,拥有在一个函数内暂停和恢复代码的执行能力,比如可以用它来自定义迭代器和实现协程。
7.2 基础
声明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16//生成器函数声明
function* generatorFn()
//生成器函数表达式
let generatorFn = function* (){}
//作为对象字面量方法的生成器函数
let foo = {
* generatorFn(){}
}
//作为类实例方法的生成器函数
class Foo {
* generatorFn(){}
}
//作为类静态方法的生成器函数
class Bar {
static * generatorFn(){}
}标识生成器函数的星号不受两侧空格的影响
注意!箭头函数不能用来定义生成器函数
暂停与恢复:一开始生成器对象处于暂停(suspended)状态。生成器与迭代器相似,它使用next()方法,让生成器恢复执行
1
2
3
4function* generatorFn(){}
const g = generatorFn()
console.log(g) //generatorFn(<suspended>)
console.log(g.next) //f next() {native code}next方法返回值
{done:true/false,value:xxx}
。
调用一次next会让生成器到达done:true
状态1
console.log(g.next()) //{done:true,value:undefined}
value可以通过return指定
1
2
3
4
5function* generatorFn(){
return 'foo'
}
let generatorOj = generatorFn()
console.log(generatorOj.next()) //{done:true,value:'foo'}生成器只会在初次调用next方法后开始执行
7.3 yield
yield 可以让生成器停止和开始执行,生成器遇到yield之前会正常执行,遇到yield后会停止执行,停止执行的生成器函数只能通过next方法恢复执行
1
2
3
4
5
6
7
8
9
10function* generatorFn(){
yield 'foo';
yield 'bar';
return 'baz';
}
let generatorOj = generatorFn()
console.log(generatorOj.next()) //{done:false,value: 'foo'}
console.log(generatorOj.next()) //{done:false,value:'bar'}
console.log(generatorOj.next()) //{done:true,value:'baz'}通过yield关键字退出的生成器函数会处于done:false状态;通过return关键字退出的生成器函数会处于done:true状态
生成器对象可迭代
1
2
3
4
5
6
7
8
9
10
11function* generatorFn(){
yield 1;
yield 2;
yield 3;
}
for(const x of generatorFn()){
console.log(x)
}
//1
//2
//3使用yield实现输入和输出
1
2
3
4
5
6
7
8
9function* generator(initial){
console.log(initial)
console.log(yield)
console.log(yield)
}
let generatorOj = generatorFn('foo')
generatorOj.next('bar') //foo
generatorOj.next('baz') //baz
generatorOj.next('qux') //qux第一次调用next传入的值不会被使用,因为第一次调用是为了开始执行生成器函数
1
2
3
4
5
6function* generatorFn(){
return yield 'foo'
}
let generatorOj = generatorFn()
console.log(generatorOj.next()) //{done:false,value:'foo'}
console.log(generatorOj.next('bar')) //{done:true,value:'bar'}yield* 增强行为,
- 它能迭代一个可迭代对象,从而一次产出一个值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//等价的generatorFn
//function* generatoFn(){
// for(const x of [1, 2, 3]){
// yield x;
// }
//}
function* generatorFn(){
yield* [1, 2, 3]
}
let generatorOj = generatorFn()
for(const x of generatorOj){
console.log(x)
}
//1
//2
//3 - yield*的值是关联迭代器返回done:true时value属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39//对于普通迭代器来说,这个值时undefined
function* generatorFn(){
console.log('iter value', yield* [1, 2, 3])
}
for(const x of generatorFn()){
console.log('value',x)
}
//value 1
//value 2
//value 3
//iter value, undefined
//对于生成器函数产生的迭代器来说,这个值就是生成器函数返回的值
function* innerGeneratorFn(){
yield 'foo';
return 'bar';
}
function* outerGeneratoFn(genObj){
console.log('iter value:', yield* innerGeneratorFn())
}
for(const x of outerGeneratorFn()){
console.log('value',x)
}
//value,foo
//iter value: bar
- yield* 实现递归算法
```js
functio* nTimes(n){
if(n > 0){
yield* nTimes(n-1);
yield n-1;
}
}
for(const x of nTimes(3)){
console.log(x)
}
//0
//1
//27.4 终止生成器
- 它能迭代一个可迭代对象,从而一次产出一个值
return 强制生成器进入关闭状态,只要进入关闭状态,就无法恢复。后续调用next()会显示done:true状态,提供的任何返回值都不会被存储或传播
1
2
3
4
5
6
7
8
9
10
11function* generatorFn(){
for(const x of [1, 2, 3]){
yield x;
}
}
const g = generatorFn();
console.log(g) //generatorFn (<suspended>)
console.log(g.next()) //{done:false,value:1}
console.log(g.return(4)) //{done:true,value:4}
console.log(g) //generatorFn (<closed>)
console.log(g.next()) //{done:true,value:undefined}for-of循环会忽略状态为done:true的内部返回值
throw 会在暂停的时候将一个提供的错误注入到生成器对象中,如果错误未被处理,生成器就会关闭
1
2
3
4
5
6
7
8
9
10
11
12function* generatorFn(){
for(const x of [1, 2, 3]){
yield x;
}
}
const g = generatorFn()
try{
g.throw('foo')
}catch(e){
console.log(e) //foo
}
console.log(g) //generatorFn (<closed>)不过,假设生成器内部处理了这个错误,那么生成器就可以恢复执行,错误处理会跳过对应的yield
1
2
3
4
5
6
7
8
9
10
11function* generatorFn(){
for(const x of [1, 2, 3]){
try{
yield x;
}catch(e){}
}
}
const g = generatorFn()
console.log(g.next()) //{done:false,value:1}
g.throw('foo')
console.log(g.next()) //{done:false:value:3}如果生成器对象还没开始执行,调用throw()抛出的错误就不会在函数内部被捕获,因为这相当于在函数外部抛出错误
第9章 Javascript-跨端请求
9.1 XMLHttpRequest对象
把Ajax推到历史舞台上的关键技术是XMLHttpRequest(XHR)对象。这个对象最早由微软发明,然后被其他浏览器所借鉴。在XHR出现之前,Ajax风格的通信必须通过一些黑科技实现,主要是使用隐藏的窗格或内嵌窗格。XHR为发送服务器请求和获取响应提供了合理的接口。这个接口可以实现异步从服务器获取额外数据,意味着用户点击不用页面刷新也可以获取数据。通过XHR对象获取数据后,可以使用DOM方法把数据插入网页。虽然Ajax这个名称中包含XML,但实际上Ajax通信与数据格式无关。这个技术主要是可以实现在不刷新页面的情况下从服务器获取数据,格式并不一定是XML。
它实际上是过时Web规范的产物,应该只在旧版本浏览器中使用。实际开发中,应该尽可能使用fetch()。
9.2 Ajax的限制
- 通过XHR进行Ajax通信的一个主要限制是跨源安全策略。默认情况下,XHR只能访问与发起请求的页面在同一个域内的资源。因此需要跨资源共享(CORS)
- 同源限制以及限制:同源策略(即协议 域名 端口相同),不同源三大限制:
- Cookie、LocalStorage和 IndexDB无法获得
- DOM无法获得
- AJAX请求无法发送
9.3 Fetch
- XHR对象的API被普遍认为比较难用,而Fetch API自从诞生以后就迅速成为了XHR更现代的替代标准。Fetch API支持期约(promise)和服务线程(service worker),已经成为极其强大的Web开发工具。
9.4 跨域解决方案
9.4.1 CORS
主流解决方案,支持所有类型的HTTP请求,原理浏览器会拦截ajax请求,如果发现这个ajax请求是跨域,它会在HTTP请求中加Origin
CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing)它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
普通跨域请求:只服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求:前后端都需要设置。由于同源策略的限制,所读取的cookie为跨域请求接口所在域的cookie,而非当前页
1
2
3
4
5
6
7
8//相当于ajax
fetch('/some/url',{
method:'get'
}).then(function(respone){
}).catch(function(err){
})
9.4.2 WebSocket
Web Socket(套接字)的目标是通过一个长时连接实现与服务器全双工、双向的通信。在JavaScript中创建Web Socket时,一个HTTP请求会发送到服务器以初始化连接。服务器响应后,连接使用HTTP的Upgrade头部从HTTP协议切换到Web Socket协议。这意味着Web Socket不能通过标准HTTP服务器实现,而必须使用支持该协议的专有服务器。
使用
ws://
和wss://
。前者是不安全的连接,后者是安全连接1
2
3
4
5
6
7
8
9
10
11
12
13//其中ws/wss 区别在于是否加密
var ws = new WebSocket('wws://服务器地址...')
ws.open = function(evt){
console.log('Connection open ...')
ws.send('Hello WebSockets!')
}
ws.onmessage = function(evt){
console.log('Received Message: ' + evt.data)
ws.close()
}
ws.onclose = function(evt){
console.log('Connection closed.')
}
9.4.3 JSONP
利用script标签的异步加载
JSONP 通过 script的回调函数(只支持get请求)
通常为了减轻web服务器的负载,我们把js、css,img等静态资源分离到另一台独立域名的服务器上,在html页面中再通过相应的标签从不同域名下加载静态资源,而被浏览器允许,基于此原理,我们可以通过动态创建script,再请求一个带参网址实现跨域通信。jsonp正是利用这个特性来实现的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<html>
<head>
<meta charset="utf-8">
<title>JSONP 实例</title>
</head>
<body>
<div id="divCustomers"></div>
<script type="text/javascript">
function callbackFunction(result, methodName){
var html = '<ul>';
for(var i = 0; i < result.length; i++){
html += '<li>' + result[i] + '</li>';
}
html += '</ul>';
document.getElementById('divCustomers').innerHTML = html;
}
</script>
<script type="text/javascript" src="http://www.runoob.com/try/ajax/jsonp.php? jsoncallback=callbackFunction"></script>
</body>
</html>缺点:
- 首先,JSONP是从不同的域拉取可执行代码。如果这个域并不可信
- 第二个缺点是不好确定JSONP请求是否失败。
9.4.4 Img
- 利用
<img>
标签实现跨域通信的最早的一种技术。任何页面都可以跨域加载图片而不必担心限制,因此这也是在线广告跟踪的主要方式。可以动态创建图片,然后通过它们的onload和onerror事件处理程序得知何时收到响应。
9.4.5 Hash
1 | //A页面里有个跨域的iframeB窗口 |
9.4.6 postMessage
1 | //窗口A |
第10章 Javascript-客户端存储
- 随着Web应用程序的出现,直接在客户端存储用户信息的需求也随之出现。这背后的想法是合理的:与特定用户相关的信息应该保存在用户的机器上。无论是登录信息、个人偏好,还是其他数据,Web应用程序提供者都需要有办法把它们保存在客户端。对该问题的第一个解决方案就是cookie
10.1 cookie
HTTP cookie通常也叫作cookie,最初用于在客户端存储会话信息。这个规范要求服务器在响应HTTP请求时,通过发送Set-Cookie HTTP头部包含会话信息。例如,下面是包含这个头部的一个HTTP响应:
1
2
3
4HTTP/1.1200 OK
Content-type: text/html
Set-Cookie: name = value
Other-header: other-header-value这个HTTP响应会设置一个名为”name”,值为”value”的cookie。名和值在发送时都会经过URL编码。浏览器会存储这些会话信息,并在之后的每个请求中都会通过HTTP头部cookie再将它们发回服务器,比如:
1
2
3GET /index.jsl HTTP/1.1
Cookie: name=value
Other-header:ohter-header-value这些发送回服务器的额外信息可用于唯一标识发送请求的客户端。
cookie是与特定域绑定的。设置cookie后,它会与请求一起发送到创建它的域。这个限制能保证cookie中存储的信息只对被认可的接收者开放,不被其他域访问。如果cookie总数超过了单个域的上限,浏览器就会删除之前设置的cookie。如果创建的cookie超过最大限制,则该cookie会被静默删除。
cookie构成,这些参数在Set-Cookie头部中使用分号加空格隔开,比如:
1
2
3
4HTTP/1.1200 OK
Content-type:text/html
Set-Cookie:name=value;expires=Mon,22-Jan-0707:20:24GMT;domain=.wrox.com;path=/;secure;
Other-header:ohter-header-value这个头部设置一个名为”name”的cookie,这个cookie在2007年1月22日7:10:24过期对所有wrox.com的子域及该域中的所有页面有效(通过path=/指定)。不过,这个cookie只能在SSL连接上发送,因为设置了secure标志。安全标志secure是cookie中唯一的非名/值对,只需一个secure就可以了。
要知道,域、路径、过期时间和secure标志用于告诉浏览器什么情况下应该在请求中包含cookie。这些参数并不会随请求发送给服务器,实际发送的只有cookie的名/值对。
在JavaScript中处理cookie比较麻烦,因为接口过于简单,只有BOM的document.cookie属性。所有名和值都是URL编码的,因此必须使用decodeURIComponent()解码。在所有这些参数中,只有cookie的名称和值是必需的。
1
document.cookie = encodeURIComponent("name") + "=" + encodeURIComponent("Nick");
虽然直接设置也可以,因为不需要在名称或值中编码任何字符,但最好还是使用encodeURIComponent()对名称和值进行编码
还有一种叫作HTTP-only的cookie。HTTP-only可以在浏览器设置,也可以在服务器设置,但只能在服务器上读取,这是因为JavaScript无法取得这种cookie的值。
因为所有cookie都会作为请求头部由浏览器发送给服务器,所以在cookie中保存大量信息可能会影响特定域浏览器请求的性能。保存的cookie越大,请求完成的时间就越长。即使浏览器对cookie大小有限制,最好还是尽可能只通过cookie保存必要信息,以避免性能问题。
10.2 WebStorage
Web Storage的目的是解决通过客户端存储不需要频繁发送回服务器的数据时使用cookie的问题。
localStorage是永久存储机制,sessionStorage是跨会话的存储机制,这跟浏览器关闭时会消失的会话cookie类似。这两种浏览器存储API提供了在浏览器中不受页面刷新影响而存储数据的两种方式
通过WebStorage写入的任何数据都可以立即被读取。
Storage类型用于保存名/值对数据,调用接口如下:
- clear()
- getItem(name)
- key(index)
- removeItem(name)
- setItem(name,value)
getItem()、removeItem()和setItem()方法可以直接或间接通过Storage对象调用
注意使用时建议统一使用try-catch封装
10.3 Cookie 与 Local storage 与 Session storage 区别
特性 | Cookie | localStorage | sessionStorage |
---|---|---|---|
数据的生命期 | 一般由服务器生成,可设置失效时间。如果在浏览器端生成Cookie,默认是关闭浏览器后失效 | 除非被清除,否则永久保存 | 仅在当前会话下有效,关闭页面或浏览器后被清除 |
存放数据大小 | 4K左右 | 一般为 5MB | row 2 col 2 |
与服务器端通信 | 每次都会携带在HTTP头中,如果使用cookie保存过多数据会带来性能问题 | 仅在客户端(即浏览器)中保存,不参与和服务器的通信 | 同左 |
易用性 | 需要程序员自己封装,源生的Cookie接口不友好 | 需要程序员自己封装,源生的Cookie接口不友好 源生接口可以接受,亦可再次封装来对Object和Array有更好的支持 | 同左 |
可以使用如下代码监听storage事件:对于sessionStorage和localStorage上的任何更改都会触发storage事件,但storage事件不会区分这两者。
1
window.addEventListener("storage",(event)=>console.log(${event.domain}))
有了这些存储手段,就可以在客户端通过使用JavaScript存储可观的数据。因为这些数据没有加密,所以要注意不能使用它们存储敏感信息。
10.4 IndexDB
- IndexedDB是类似于SQL数据库的结构化数据存储机制。不同的是,IndexedDB存储的是对象,而不是数据表。对象存储是通过定义键然后添加数据来创建的。游标用于查询对象存储中的特定数据,而索引可以针对特定属性实现更快的查询。
第11章 Javascript-模块化
- CommonJS是以服务器为目标环境,一次性把所有模块加载到内存中
- AMD为异步模块定义,是以浏览器为目标执行环境,需要考虑网络延迟问题,运行加载器库控制何时加载模块,实现核心使用函数包装模块定义,防止声明全局变量,包装函数也便于模块代码移植。
- UMD是为了通一ComomonJS和AMD生态系统的通用模块定义。它可以创建两个系统都可以使用的模块代码。本质上是定义的模块会在启动时检测要使用哪个模块系统,然后进行适当的配置,实现两个生态共存。
- ES6独特之处是既可以通过浏览器原生加载,也可以与第三方加载器和构建工具一起加载。浏览器可以从顶级模块加载整个依赖图,而且是异步完成的。模块文件按需加载,且后续模块的请求会因为每个依赖模块的网络延迟而同步延迟。即,如果moduleA依赖moduleB, moduleB依赖moduleC。浏览器在对moduleB的请求完成之前并不知道要请求moduleC。这种加载方式效率很高,也不需要外部工具,但加载大型应用程序的深度依赖图可能要花费很长时间。
- 总结:多年以来,CommonJS和AMD这两个分别针对服务器端环境和受延迟限制的客户端环境的模块系统长期分裂。两个系统都获得了爆炸性增强,但为它们编写的代码则在很多方面不一致,经常也会带有冗余的样板代码。而且,这两个系统都没有在浏览器中实现。缺乏兼容导致出现了相关工具,从而让在浏览器中实现模块模式成为可能。ECMAScript 6规范重新定义了浏览器模块,集之前两个系统之长于一身,并通过更简单的声明性语法暴露出来。浏览器对原生模块的支持越来越好,但也提供了稳健的工具以实现从不支持到支持ES6模块的过渡。
第12章 Javascript-性能优化
第13章 Javascript-Web安全
XSS
- XSS即为(Cross Site Scripting)跨站脚本。目标浏览器网站作用域下嵌入一段远程或第三方域上面的JS代码。防御思路:对输入(URL参数进行过滤),对输出进行编码。即对提交的所有内容进行过滤,过滤掉导致脚本执行的所有内容,然后对动态输出到页面的内容进行html编码,使脚本无法在浏览器中执行。
CSRF
- CSRF(Cross Site Request Forgery)跨站请求伪造。受害者在访问一个网站时,在其Cookie还没有过期的情况下,攻击者伪造一个链接地址发送给受害者并欺骗让其点击。防御思路:在请求地址中添加token并验证;验证HTTP Referer字段;在HTTP头重自定义属性并验证。
第14章 Javascript-算法
防抖节流
防抖
原理:每次触发事件之前都取消之前的延时调用方法
1
2
3
4
5
6
7function debounce(fn,time){
let timeout = null
return function(){
clearTimeout(timeout)
timeout = setTimeout(fn,time)
}
}
节流
原理:每次触发时判断当前是否有等待执行的延时函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function throttle(fn,delay){
let canRun = true
return function(){
if(!canRun){
//休息时间暂不接客
return false
}
//工作时间,执行函数
canRun = false
setTimeout(()=>{
fn()
canRun = true
},delay)
}
}
深浅拷贝
对象浅拷贝
1
Object.assign(target,source)
对象深拷贝
1
2
3
4
5
6
7
8function deepClone(obj){
if(!obj || typeof obj !== 'object') return obj
let newObj = Array.isArray(obj)? [] :{}
for(let key in obj){
newObj[key] = typeof obj[key] === 'object'? deepClone(obj):obj[key]
}
return newObj
}
排序算法
深度遍历和广度遍历
二分查找
二叉树
第14章 Javascript-网页性能优化
性能监控
网络层面
- 请求过程优化:Gzip压缩
- 减少网络请求(本地存储)
- 浏览器缓存机制
- 离线存储技术
渲染层面
- 服务端渲染
- 浏览器渲染机制解析
- CSS性能方案
- js性能方案
- DOM优化:回流和重绘
- 首屏渲染提速:懒加载
第15章 Javascript-ES2018和ES2019
数组打平
原本需要迭代进行
1
2
3
4
5
6
7
8
9
10
11//depth需要打平的级别
function flatten(sourceArr, depth = 1, flattenArr = []) {
for (const element of sourceArr) {
if (Array.isArray(element) && depth > 0) {
flatten(element, depth - 1, flattenArr)
} else {
flattenArr.push(element)
}
}
return flattenArr;
}ES9 新增Array.prototype.flat()
1
2
3
4
5const arr = [[0],1,2,[3,[4,5]],6]
console.log(arr.flat(2))
//[0,1,2,3,4,5,6]
console.log(arr.flat())
//[0,1,2,3,[4,5],6]Array.prototype.flatMap()方法会在打平数组之前执行一次映射操作。在功能上,arr.flatMap(f)与arr.map(f).flat()等价;但arr.flatMap()更高效,因为浏览器只需要执行一次遍历。
1
2
3
4
5const arr = [[1],[3],[5]]
console.log(arr.map(([x])=>[x,x+1]))
//[[1,2],[3,4],[5,6]]
console.log(arr.flatMap(([x])=>[x,x+1]))
//[1,2,3,4,5,6]
Object.fromEntries()
ECMAScript 2019又给Object类添加了一个静态方法fromEntries(),用于通过键/值对数组的集合构建对象。这个方法执行与Object.entries()方法相反的操作
1
2
3
4
5
6
7
8
9const obj = {
foo:'bar',
baz:'qux'
}
const objEntries = Object.entries(obj)
console.log(objEntries)
//[["foo","bar"],["baz","qux"]]
console.log(Object.fromEntries(objEntries))
//["foo":"bar","baz":"qux"]这个方法可以方便将Map实例转换为Object实例
1
2
3const map = new Map().set('foo','bar')
console.log(Object.fromEntries(map))
//{foo:'bar'}
处理开头末尾空格
ES9 添加了trimStart()和trimEnd(),它们分别用于删除字符串开头和末尾空格。
1
2
3let s = ' foo ';
console.log(s.trimStart()) //'foo '
console.log(s.trimEnd()) //' foo'这两个方法相当于执行与padStart()和padEnd()相反的操作
1
2
3
4
5
6let s = 'x'
console.log(s.padStart(4,"ab"))
//'abax'
console.log(s.padStart(1,"bbb"))
//'x'
//如果原字符串的长度,等于或大于指定的最小长度,则返回原字符串Symbol.prototype.description
ES9新增了Symbol.prototype.description,用于取得Symbol的符号描述
1
2
3
4
5
6
7
8//以前
const s = Symbol('foo')
console.log(s.toString())
//Symbol(foo)
//现在
console.log(s.description)
//foo
省略catch的错误对象
1
2
3
4
5
6
7
8
9
10
11
12//以前
try{
throw 'foo'
}catch(e){
//发生错误,但是你不想使用错误对象
}
//现在
try{
throw 'foo'
} catch{
}