JavaScript 基础-总览(第零章)

1.js运行环境

js作为脚本语言运行在浏览器中,浏览器就是js的运行环境。对于众多风云的浏览器厂商来说, 他们的内核又是不一样的。浏览器内核分为两种:渲染引擎和js引擎。

  • 渲染引擎:负责网页内容呈现的
  • Js引擎:解释js脚本,实现js交互效果的

1.1常见的内核

主流浏览器 内核
IE -> Edge trident -> EdgeHTML
chrome webkit -> blink
safari webkit
Firefox Gecko
Opera Presto -> blink

1.2 现在我们有一个js文件,那么浏览器是如何执行它的呢?

首先我们js文件以scirpt标签元素呈现在html里面的。浏览器根据html文件以此解析标签,当解析到scirpt标签时,会停止html解析,阻塞住,开始下载js文件并且执行它,在执行的过程中,如果是第一个js文件此时浏览器会触发首次渲染(至于为什么,自己做下实验,不懂的可以留言)。所以出现一个问题js文件大大阻碍了html页面解析及渲染,所以引入async和defer两个属性(对于 首屏优化有很大的提升,也要谨慎使用)

  • async:开启另外一个线程下载js文件,下载完成,立马执行。(此时才发生阻塞)
  • defer:开启另一个线程下载js文件,直到页面加载完成时才执行。(根本不阻塞)

2.js数据类型

2.1 基本数据类型:

  • string:由多个16位Unicode字符组成的字符序列,有单引号或双引号表示
  • number:采用了IEEE754格式来表示整数和浮点数值
  • boolean:有两个字面值,true和false.区分大小写的
  • null:只有一个值的数据类型,值为null.表示一个空对象指针,但用typeof操作会返回一个对象。一般我们把将来用于保存对象的变量初始化为null.
  • undefined:这个类型只有一个值,在声明变量未进行赋值时,这个变量的值就是undefined.
  • Symbol:唯一的值。

2.2 引用数据类型:

object:就是一组数据和功能的集合,无序的键值对的方式存储。可以通过new操作符和创建对象构造函数 来创建。常见的对象类型有array,date,function等.

2.3 经典面试题:

0.1+0.2为什么不等于0.3?

0.1和0.2在转换成二进制后会无限循环,由于标准位数的限制后面多余的位数会被截掉,此时就已经出现了精度的损失,相加后因浮点数小数位的限制而截断的二进制数字在转换为十进制就会变成 0.30000000000000004。

2.4 数据类型检测方式:

2.4.1. typeof

typeof检测null是一个对象
typeof检测函数返回时一个function
typeof检测其他对象都返回 object

2.4.2. instanceof

只要在当前实例的原型链上,用instanceof检测出来的结果都是true,所以在类的原型继承中,最后检测 出来的结果未必是正确的.而且instanceof后面必须更一个对象。
不能检测基本类型

2.4.3. constructor:

每个构造函数的原型对象都有一个constructor属性,并且指向构造函数本身,由于我们可以手动修改 这个属性,所以结果也不是很准确。 不能检测null和undefined

2.4.4. Object.prototype.toString.call(最佳方案)

调用Object原型上的toString()方法,并且通过call改变this指向。返回的是字符串

3.js类型转换

3.1. 转换方式

javaScript作为一门弱类型语言,本质为一个变量可以被赋予不同的数据类型。代码简洁灵活,但稍有 不慎,会出现很多坑。
javaScript也作为一门动态类型语言,在运行时,可以随便改变其变量的结构。
所以js变量可以做任意的类型转换,有两种方式,显示类型转换和隐士类型转换。
但是能转换的类型只有三种:to Number,to String,to Boolean.
当基本类型转换成上述类型时会调用:Number() ,String(), Boolean()
只有’’、0、null、undefined、NaN、false转换boolean为false,其他都为true
当引用类型转换时,就稍微有些复杂,我们来举个例子:(所有对象转换boolean都为true)

1
2
3
let obj={ value:'你好啊', num:2, toString:function(){ return this.value }, valueOf:function(){ return this.num } } console.log(obj+'明天') //2明天 
console.log(obj+1) // 3
console.log(String(obj)) // 你好啊

3.2. 转换流程

当对象进行类型转换时:

  1. 首先调用valueOf,如果执行结果是原始值,返回,如果不是下一步
  2. 其次调用toString,如果执行结果是原始值,返回,如果不是,报错。
    特殊情况:
    当使用显示类型转换成String时,执行顺序则是先调用toString,其次调用valueOf
    显示类型转换:
    Number() / parseFloat() / parseInt()/String() / toString()/Boolean()
    隐式类型转换:
    + - == !><= <= >=

3.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
1 + '1' 
true + 0
{}+[]
4 + {}
4 + [1]
'a' + + 'b'
console.log ( [] == 0 )
console.log ( ! [] == 0 )
console.log ( [] == ! [] )
console.log ( [] == [] )
console.log({} == !{})
console.log({} == {})

// 答案:
'11'
1
0
"4[object Object]"
"41"
"aNaN"
true
true
true
false
false
false

4.js遍历

4.1对象遍历:

  1. for in:自身和继承属性,可枚举,不含Symbol
  2. Object.keys(obj):可枚举,不含Symbol,自身
  3. Object.values(obj):可枚举,不含Symbol,自身
  4. Object.getOwnPropertyNames(obj):自身所有属性,不含Symbol
  5. Reflect.ownKeys(obj):自身所有属性

4.2 数组遍历:

forEach,map,filter,every,some,reduce等

4.3 字符串遍历:

for in

4.4 Set数据结构:

  • Set.prototype.keys():返回键名的遍历器
  • Set.prototype.values():返回键值的遍历器
  • Set.prototype.entries():返回键值对的遍历器
  • Set.prototype.forEach():回调函数遍历每个成员

4.5 Map数据结构:

  • Map.prototype.keys():返回键名的遍历器
  • Map.prototype.values():返回键值的遍历器
  • Map.prototype.entries():返回键值对的遍历器
  • Map.prototype.forEach():回调函数遍历每个成员

5. 作用域与作用域链

5.1. 作用域

javascript采用的静态作用域,也可以称为词法作用域,意思是说作用域是在定义的时候就创建了, 而不是运行的时候。如下:

1
2
3
4
5
6
7
8
9
10
let a=1 
function aa() {
console.log(a) //输出1
}
function bb() {
let a=2
aa()
}

bb() //输出1

aa在bb里面调用的,aa函数里面没有a变量,那么就应该去调用它的作用域里找,刚好找到a等于2。可是js采用的静态作用域,不管怎么运行,定义的时候作用域已经生成了

变量和函数能被有效访问的区域或者集合。作用域决定了代码块之间的资源可访问性。 作用域也就是一个独立的空间,用于保护变量防止泄露,也起到隔离作用。每个作用域里的变量可以相同命名,互不干涉。就像一栋房子一样,每家每户都是独立的,就是作用域。 作用域又分为全局作用域和函数作用域,块级作用域。 全局作用域任何地方都可以访问到,如window,Math等全局对象。 函数作用域就是函数内部的变量和方法,函数外部是无法访问到的。 块级作用域指变量声明的代码段外是不可访问的,如let,const.

5.2. 作用域链

表示一个作用域可以访问到变量的一个集合。函数作为一个对象有一个[[scope]]属性,就是表示这个集合的。

  • AO:活动变量(Active object,VO)
  • VO:变量对象(Variable object,VO)
  • 执行上下文:代码运行的环境,分为全局上下文和函数上下文

举个例子:

1
2
3
4
5
6
7
8
9
10
function a() {
function b() {
var b = 234;
}
var a = 123;
b();
}

var gloab = 100;
a();

1. a 函数定义

a 函数在被定义时,a函数对象的属性[[scope]]作用域指向他的作用域链scope chain,此时它的作用域链的第一项指向了GO(Global Object)全局对象,全局对象上此时有5个属性,分别是this、window、document、a、glob。
01

2. a 函数执行

当a函数被执行时,此时a函数对象的作用域[[scope]]的作用域链scope chain的第一项指向了AO(Activation Object)活动对象,AO对象里有4个属性,分别是this、arguments、a、b。第二项指向了GO(Global Object),GO对象里依然有5个属性,分别是this、window、document、a、golb

02

3. b 函数定义

当b函数被定义时,此时b函数对象的作用域[[scope]]的作用域链scope chain的第一项指向了AO(Activation Object)活动对象,AO对象里有4个属性,分别是this、arguments、a、b。第二项指向了GO(Global Object),GO对象里依然有5个属性,分别是this、window、document、a、golb

03

4. b 函数执行

当b函数被执行时,此时b函数对象的作用域[[scope]]的作用域链scope chain的第一项指向了AO(Activation Object)活动对象,AO对象里有3个属性,分别是this、arguments、b。第一项指向了AO(Activation Object)活动对象,AO对象里有4个属性,分别是this、arguments、a、b。第二项指向了GO(Global Object),GO对象里依然有5个属性,分别是this、window、document、a、golb。 以上就是上面代码执行完之后的结果。

04

6. 闭包

6.1. 定义

  • mdn:闭包是指那些能够访问自由变量的函数
  • 维基百科:在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。闭包可以用来在一个函数与一组“私有”变量之间创建关联关系。在给定函数被多次调用的过程中,这些私有变量能够保持其持久性
  • 一个作用域可以访问另一个作用域的变量,就产生闭包。之前比喻作用域就好比一栋房子每一户,闭包相当于串门
  • ECMAScript:
    • 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
    • 从实践角度:即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回),在代码中引用了自由变量

6.2. 自由变量

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。即闭包=函数+函数能够访问的自由变量。
从技术的角度讲,所有的JavaScript函数都是闭包。

1
2
3
4
5
6
var a = 1;
function foo() {
console.log(a);
}

foo()

foo 函数可以访问变量 a,但是 a 既不是 foo 函数的局部变量,也不是 foo 函数的参数,所以 a 就是自由变量。

6.3. 举例

1
2
3
4
5
6
7
8
9
10
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
var foo = checkscope();
foo(); // 输出 "local scope"

因为变量查找的规则是通过作用域链的,作用域链是在函数定义的时候就已经确定了, 所以我们来看看定义f函数时候的[[scope]]属性:

1
2
3
4
5
6
7
8
9
10
[
AO:{
scope:"local scope",
f:function
},
global:{
scope :"local scope",
checkscope:function
}
]

f执行时候的[[scope]]属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[
AO:{
arguments:[],
this:window
},
AO:{
scope:"local scope",
f:function
},
global:{
scope :"local scope",
checkscope:function
}
]
// 根据先后顺序scope变量输出为"local scope"

6.4. 面试题

1
2
3
4
5
6
7
8
9
10
11
var data = [];

for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
// 答案是都是 3

原理;当执行到 data[0] 函数之前,此时全局上下文的 VO 为:

1
2
3
4
5
6
globalContext = {
VO: {
data: [...],
i: 3
}
}

当执行 data[0] 函数的时候,data[0] 函数的作用域链为:

1
2
3
data[0]Context = {
Scope: [AO, globalContext.VO]
}

data[0]Context 的 AO 并没有 i 值,所以会从 globalContext.VO 中查找,i 为 3,所以打印的结果就是 3

7. 原型及原型对象

javascript万物皆对象,

  • 每个对象都有一个__proto__属性,指向了创造它的构造函数的原型对象。
  • 每个函数都有一个原型对象prototype,当使用 new 创造对象时继承这个对象。
    1
    2
    3
    function A(){} 
    var a = new A()
    a.__proto__ === A.prototype
    那么,谁创造了A这个构造函数呢,还有谁创造了A.prototype这个对象呢?这时候我们就要知道js两个顶级函数,FunctionObject

7.1. Function

所有函数都是由 Function 创建的,也包括自己,也就是说 Function 创造了自己。

1
2
3
A.__proto__ === Function.prototype
Function.__proto__ === Function.prototype
Object.__proto__ === Function.prototype

7.2. Object

所有的对象都是由 Object 构造函数创建的,Object 没有人创建

1
2
A.prototype.__proto__ === Object.prototype
Object.prototype.__proto__===null

7.3. 原型链(一种访问机制)

  • 在访问对象的某个成员的时候会先在对象中找是否存在
  • 如果当前对象中没有就在构造函数的原型对象中找
  • 如果原型对象中没有找到就到原型对象的原型上找
  • 直到Object的原型对象的原型是null为止

05

8. this指向问题

this指向一共有七种情况

8.1. 全局环境:普通函数调用,普通对象

1
2
3
4
5
const obj={a:this}
obj.this===window //true
function fn(){
console.log(this) //window
}

8.2. 构造函数

1
2
3
4
5
6
// new出来的对象,this指向了即将new出来的对象。当做普通函数执行,this指向window。
function a() {
console.log(this)
}
const obj = new a() // a{}
a() // 'window'

8.3. 对象方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 作为对象方法,this指向了这个对象。(新对象绑定到函数调用的this)。一旦有变量直接指向了这个方法,this为window.
const obj = {
x: 0,
foo: function () {
console.log(this)
}
}
obj.foo() // obj
const a = obj.foo
a() //window
// 特殊情况,如果在方法里面执行函数,this指向window.
const obj = {
x: 0,
foo: function () {
console.log(this) // obj
function foo1() {
console.log(this) //window
}
foo1()
}
}
obj.foo()

8.4. 构造函数prototype属性

1
2
3
4
5
6
7
8
9
10
// 原型定义方法的this指向了实例对象。毕竟是通过对象调用的。
function Fn() {
this.a = 10
let a = 100
}
Fn.prototype.fn = function () {
console.log(this.a) // 10 说明指向了obj这个对象
}
const obj = new Fn()
obj.fn()

8.5. call ,apply, bind

1
2
3
4
5
6
7
8
9
10
// this指向传入的对象。
const obj = {
x: 10
}
function fn() {
console.log(this)
}
fn.call(obj) //obj
fn.apply(obj) //obj
fn.bind(obj)() //obj

8.6. DOM事件

1
2
3
4
5
// 指向绑定事件的对象。
document.getElementById('app')
.addEventListener('click', function () {
console.log(this) // id为app的这个对象
})

8.7. 箭头函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
obj = {
a: 10,
c: function () {
b = () => {
console.log(this) //指向obj
}
b()
}
}
obj.c()

// 在方法中定义函数应该是指向window,但是箭头函数没有自己的this,所以指向上一层作用域中的this.
document.getElementById('app')
.addEventListener('click', () => {
console.log(this) // 改为箭头函数,指向了window,而不是触发对象
})

8.8. 绑定方式

  • 隐士绑定:谁调用方法,this指向谁。
  • 显示绑定:call,bind,apply
  • new 绑定
  • 优先级问题:new>显示绑定>隐式绑定

9. 继承

9.1. 原型链继承

缺点:多个实例对引用类型操作会被篡改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Animal() {
this.name = 'cat'
this.msg = {
age: 9
}
}
Animal.prototype.greet = function () {
console.log('hehe')
}
function Dog() {
this.name = 'dog'
}
Dog.prototype = new Animal() //核心一步

const a = new Dog()
a.msg.age = '99'
const b = new Animal()

9.2. 构造函数继承

缺点:

  • 只能继承父类的实例属性和方法,不能继承原型属性/方法。
  • 性能不好,每个子类都会拥有父类实例的副本。
1
2
3
4
5
6
7
8
9
10
11
12
13
function Animal() {
this.name = 'cat'
this.msg = {
age: 9
}
}
Animal.prototype.greet = function () {
console.log('hehe')
}
function Dog() {
Animal.call(this) // 核心一步
}
const a=new Dog()

9.3. 组合继承

就是将上两种方法结合起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Animal() {
this.name = 'cat'
this.msg = {
age: 9
}
}
Animal.prototype.greet = function () {
console.log('hehe')
}
function Dog() {
Animal.call(this) // 核心一步
}
Dog.prototype = new Animal() // 核心一步
const a=new Dog()

9.4. 原型式继承

利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型。

缺点:

  • 不能做到函数复用
  • 共享引用类型属性的值
  • 无法传递参数
1
2
3
4
5
6
7
8
9
10
11
12
13
function inheritObject(obj){
function F(){};
F.prototype = obj;
return new F();
}

var situation = {
companies:['bigo','yy','uc'];
area:'guangzhou';
}

var situationA = inheritObject(situation);
console.log(situationA.area) //'guangzhou'

9.5. 寄生式继承

在原型式继承的基础上,增强对象,返回构造函数.
缺点同上

1
2
3
4
5
6
7
8
9
10
11
12
13
 function createAnother(original){
var clone = object(original); // 或 Object.create(original)
clone.sayHi = function(){ // 以某种方式来增强对象
alert("hi");
};
return clone; // 返回这个对象
}
var person = {
name: 'Nicholas',
friends : ["Shelby","Coury","Van"]
}

var anotherPerson = createAnother(person)

9.6. extends(es6)

9.6.1. 写法

1
2
3
4
5
6
// 写法
class Point {
}

class ColorPoint extends Point {
}

上面代码定义了一个ColorPoint类,该类通过extends关键字,继承了Point类的所有属性和方法

1
2
Object.getPrototypeOf可以使用这个方法判断,一个类是否继承了另一个类
Object.getPrototypeOf(ColorPoint) === Point //true

9.6.2.Super关键字

1
2
3
4
5
6
7
8
// 子类B的构造函数之中的super(),代表调用父类的构造函数。这是必须的,否则 JavaScript 引擎会报错。
class A {}

class B extends A {
constructor() {
super();
}
}

注意,super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B的实例,因此super()在这里相当于 A.prototype.constructor.call(this)。

1
2
3
4
5
6
7
8
9
10
11
12
13
// new.target指向当前正在执行的函数。可以看到,在super()执行时,它指向的是子类B的构造函数,而不是父类A的构造函数。也就是说,super()内部的this指向的是B。
class A {
constructor() {
console.log(new.target.name);
}
}
class B extends A {
constructor() {
super();
}
}
new A() // A
new B() // B

作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。

1
2
3
4
5
6
7
8
// super()用在B类的m方法之中,就会造成语法错误
class A {}

class B extends A {
m() {
super(); // 报错
}
}

第二种情况,super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 子类B当中的super.p(),就是将super当作一个对象使用。这时,super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()
class A {
p() {
return 2;
}
}

class B extends A {
constructor() {
super();
console.log(super.p()); // 2
}
}

let b = new B();

这里需要注意,由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// p是父类A实例的属性,super.p就引用不到它
class A {
constructor() {
this.p = 2;
}
}

class B extends A {
get m() {
return super.p;
}
}

let b = new B();
b.m // undefined

如果属性定义在父类的原型对象上,super就可以取到。

1
2
3
4
5
6
7
8
9
10
11
12
// 属性x是定义在A.prototype上面的,所以super.x可以取到它的值
class A {}
A.prototype.x = 2;

class B extends A {
constructor() {
super();
console.log(super.x) // 2
}
}

let b = new B();

ES6 规定,在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// super.print()虽然调用的是A.prototype.print(),但是A.prototype.print()内部的this指向子类B的实例,导致输出的是2,而不是1。也就是说,实际上执行的是super.print.call(this)。
class A {
constructor() {
this.x = 1;
}
print() {
console.log(this.x);
}
}

class B extends A {
constructor() {
super();
this.x = 2;
}
m() {
super.print();
}
}

let b = new B();
b.m() // 2

由于this指向子类实例,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// super.x赋值为3,这时等同于对this.x赋值为3。而当读取super.x的时候,读的是A.prototype.x,所以返回undefined。
class A {
constructor() {
this.x = 1;
}
}

class B extends A {
constructor() {
super();
this.x = 2;
super.x = 3;
console.log(super.x); // undefined
console.log(this.x); // 3
}
}

let b = new B();

如果super作为对象,用在静态方法之中,这时super将指向父类,而不是父类的原型对象。

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
// super在静态方法之中指向父类,在普通方法之中指向父类的原型对象
class Parent {
static myMethod(msg) {
console.log('static', msg);
}

myMethod(msg) {
console.log('instance', msg);
}
}

class Child extends Parent {
static myMethod(msg) {
super.myMethod(msg);
}

myMethod(msg) {
super.myMethod(msg);
}
}

Child.myMethod(1); // static 1

var child = new Child();
child.myMethod(2); // instance 2

在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例

10. 数据存储与传参

在JavaScript中,每一个变量在内存中都需要一个空间来存储。内存空间又分为栈内存和堆内存。 基本数据类型保存在栈中,引用类型保存于堆中。

1
2
3
let a='a'
a='b'
let b=a
  • 首先创建变量a,在创建字符串’a’,使变量a指向’a’,a保存的是这个值
  • 接着又创建字符串’b’,使变量a指向字符串’b’,同时删除’a’
  • 深拷贝了一份a(重新创建了’b’),使变量b指向了’b’,变量a和变量b互不受影响。
1
2
3
4
let obj1=new Object()
let obj2=obj1
obj1.name='node'
alert(obj2.name) //node
  • 创建一个对象,开辟了一个堆内存,使变量obj1指向这个对象(堆内存)的地址
  • 创建一个变量obj2,将obj1的值赋值给obj2,就是将地址赋值给了obj2,这时obj1和obj2指向的是同一个堆内存
  • 两个变量就相互影响。

10.2. 传递参数

所有函数的参数都是按值传递的。也就是说,函数外部的值。复制给函数内部的参数.

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
function add(num){
num+=10
return
}
var counrt=20
var result=add(counrt)
alert(counrt) //20,没有变化
alert(result) //30

function setName(obj){
obj.name='node'
}
var person=new Object()
setName(person)

alert(person.name) //'node'

function setName(obj){
obj.name='node'
obj={name:'java'}
}
var person=new Object()
setName(person)

alert(person.name) //'node'

之前说过,函数参数是按值传递的。所以在函数内部,obj这个局部变量保存的是person这个对象的地址。第一步操作了这个地址下的name属性为’node’,第二步,就是将这个变量指向了一个新的堆地址,所以外部person对象丝毫不受影响,name属性依旧为node.

11. 深拷贝与浅拷贝(手写一个深拷贝)

由于数据存储方式不同,对于引用数据而言,有了浅拷贝和深拷贝。浅拷贝是指拷贝地址,公用同一个堆内存,两个变量相互受影响,深拷贝使指,开辟一块内存空间,保存相同的值。互不受影响。

11.1. 浅拷贝

Object.assign:,contact,扩展运算符等。

11.2. 深拷贝

11.2.1. JSON

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const obj = {
arr: [1, 1],
obj: { key: 11 },
a: () => { },
date: new Date(),
reg: /reg/g,
null: null,
undefined: undefined
}
obj1 = JSON.parse(JSON.stringify(obj))
console.log(obj1)
console.log(obj1 === obj)
console.log(obj1.obj === obj.obj)

// 输出结果,函数和undefine都不见了,正则变成一个空对象了
{
arr: [ 1, 1 ],
obj: { key: 11 },
date: '2020-04-15T07:13:36.087Z',
reg: {},
null: null
}
false
false

11.2.2. 递归+for in

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
function isObj (obj) {
return (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)
}
function clone (obj) {
let tempobj = Array.isArray(obj) ? [] : {}
for (let key in obj) {
tempobj[key] = isObj(obj[key]) ? clone(obj[key]) : obj[key]
}
return tempobj
}

let a = clone(obj)
console.log(a === obj)
console.log(a)

// 输出结果,无法拷贝函数,正则,日期对象。
false
{
arr: [ 1, 1 ],
obj: { key: 11 },
a: {},
date: {},
reg: {},
null: null,
undefined: undefined

11.2.3. 环

环就是对象循环引用自己

1
2
a={a:'a'}
a.c=a

而使用上面两种方法拷贝就会直接报错的。所以我们就要借鉴WeakMap数据结构,每一次拷贝的时候就去先weakMap查询该对象是否已经被拷贝,如果已经拷贝取出该对象并返回,所以我们要改造一下clone函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function clone (obj, hash = new WeakMap()) {
if (hash.has(obj)) return hash.get(obj)
let tempobj = Array.isArray(obj) ? [] : {}
hash.set(obj, tempobj)
for (let key in obj) {
tempobj[key] = isObj(obj[key]) ? clone((obj[key]), hash) : obj[key]
}
return tempobj
}
const obj = {}
obj.obj = obj
let a = clone(obj)
console.log(a)

// 输出结果,说明拷贝成功了,没有报错
{ obj: [Circular] }

解决一下,date,reg.

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
function clone (obj, hash = new WeakMap()) {
let tempobj, constructor
constructor = obj.constructor
switch (constructor) {
case RegExp:
tempobj = new constructor(obj)
break;
case Date:
tempobj = new constructor(obj)
break;
default:
if (hash.has(obj)) return hash.get(obj)
tempobj = Array.isArray(obj) ? [] : {}
hash.set(obj, tempobj)
}
for (let key in obj) {
tempobj[key] = isObj(obj[key]) ? clone((obj[key]), hash) : obj[key]
}
return tempobj
}

// 输出结果,只剩下函数没有解决了
{
arr: [ 1, 1 ],
obj: [Circular],
a: [Function: a],
date: 2020-04-15T08:14:03.307Z,
reg: /reg/g,
null: null,
undefined: undefined
}

12. 高阶函数及柯里化

高阶函数定义:接收函数作为参数或者返回函数的函数,常见的方法有:map,filter,bind,apply等。

12.1. 面试题

1
2
3
4
// 如何实现add(1)(2)(3)结果等于6
const add = x => y => z=>x+y+z
add(1)(2)(3) //6
// 一行代码就可以实现了。

12.2. 接受参数为函数

1
2
3
4
5
6
7
8
9
10
11
12
13
// 实现一下map
[1,2,3].map((item)=>item*2) //[2,4,6]
map接受一个函数作为参数,并且返回一个新的数组
Array.prototype.myMap=function(fn){
let arr=[]
 for(var i = 0; i<this.length; i++){
 arr.push(fn(this[i],i,this)) //调用传入的参数
}
return arr
}

// 测试:
[1,2,3].myMap((item)=>item*2) //[2,4,6]

12.3. 返回函数

12.4. 柯里化

把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术

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
// 如何实现add(1)(2)(3)结果等于6?
// 首先我们做的要把所有参数收集起来,然后进行统一处理
function add() {
// 第一次执行时,定义一个数组专门用来存储所有的参数
var _args = Array.prototype.slice.call(arguments)

// 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
var _adder = function() {
_args.push(...Array.from(arguments))
return _adder
};

// 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
_adder.toString = function () {
return _args.reduce(function (a, b) {
return a + b;
})
}
return _adder;
}

// 测试:
console.log(add(1)(2)(3)) // 6
console.log(add(1, 2, 3)(4)) // 10
console.log(add(1)(2)(3)(4)(5)) // 15

//注意:只有在对该函数取值时,才会进行类型转换,才会调用toString返回相加的值,否则只是调则返回函数而不是相加值

13. ajax

ajax是一系列技术的统称,实现向服务器发送数据接受数据而页面不刷新。

ajax也可以取消事件监听,将异步改为同步,调用abort()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// XMLHttpRequest是浏览器提供的一个api,在js中,也就是一个构造函数
// 简单完成一个ajax请求:
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = () => {
if (xhr.readystate == 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
alert(xhr.responseText)
} else {
alert(xhr.status)
}
}
}
xhr.open("get", "user", true)
xhr.send(null)

readystate为实例的属性:

  • 0:未初始化 – 尚未调用.open()方法
  • 1:启动 – 已经调用.open()方法,但尚未调用.send()方法;
  • 2:发送 – 已经调用.send()方法,但尚未接收到响应;
  • 3:接收 – 已经接收到部分响应数据;
  • 4:完成 – 已经接收到全部响应数据,而且已经可以在客户端使用了;

status为响应状态码(http状态),和readystate不同。

progress进度:

  • loadstart:在接收到响应数据的第一个字节时触发
  • progress:在接收响应期间持续不断地触发
  • error:在请求发生错误时触发;
  • load:在接收到完整的响应数据时触发;
  • oadend:在通信完成或者触发error,abort或load事件后触发

14. DOM

14.1. DOM 元素的增删改查

14.1.1 增加 DOM 元素

  • appendChild:向当前节点的子节点列表的末尾添加新的子节点
  • insertBefore(newchild,refchild):其中参数newchiild表示插入新的节点,refchild表示在此节点前插入新的子节点,返回新的子节点
    1
    2
    3
    4
    document.body.insertAdjacentHTML(
    'beforeend',
    '<a>你好</a>'
    )
    这个方法允许你将任何有效的 HTML 字符串插入到一个 DOM 元素的四个位置,这四个位置由方法的第一个参数指定,分别是:
  • beforebegin: 元素之前
  • afterbegin: 元素内,位于现存的第一个子元素之前
  • beforeend: 元素内,位于现存的最后一个子元素之后
  • afterend: 元素之后
    1
    2
    3
    4
    5
    6
    7
    <!-- beforebegin -->
    <div>
    <!-- afterbegin -->
    <span></span>
    <!-- beforeend -->
    </div>
    <!-- afterend -->

14.1.2 删除 DOM 元素

1
2
3
4
5
6
// 方法1:
const target = document.querySelector('.target')
target.parentNode.removeChild(target)
// 方法2:
const target = document.querySelector('#target')
target.remove()

14.1.3 移动 DOM 元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 移动前
<h1>frist</h1>
<p>1</p>
<h2>two</h2>

// 运用之前的insertAdjacentHTML方法
const h1 = document.querySelector('h1')
const h2 = document.querySelector('h2')
h1.insertAdjacentElement('afterend', h2)

// 移动后
<h1>frist</h1>
<h2>two</h2>
<p>1</p>

14.1.4 替换 DOM 元素

1
2
parentNode.replaceChild(newNode, oldNode)
oldElement.replaceWith(newElement)

14.1.5 DOM 检测:

1
2
- `matches`:以判断出一个元素是否匹配一个确定的选择器
- `contains`:可以检测出一个元素是否包含另一个元素

14.1.6 DOM 观察者:MutationObserver