JavaScript 高级

55 阅读12分钟

1 基础深入总结

1.1 基础问题总结

(1) null 和 undefined

1nullundefined 什么区别?
	undefined: 变量定义了但是没有值。
	null: 变量定义了值为 null2、 什么时候得到 undefined ?
	没有赋值的变量
    没有赋值的属性或获取对象中不存在的属性以及获取数组中不存在的元素
    没有返回值的函数,默认返回 undefined
    形参没有给对应的实参。
	
	
3、 什么时候得到 null ?
	定义了变量暂时不知道赋什么值,先赋值为 null。
	获取dom对象的时候,如果没有满足条件的元素,得到 null

(2) 变量、数据和内存

数据是存储在一块内存区域内,变量就是对这一块内存的标识。
var a = xxx, a内存中到底保存的是什么?

如果 xxx 是原始类型/值类型,变量存在栈结构中,值也存在栈结构中
如果 xxx 是对象类型/引用类型,对象存在堆结构中,变量在栈中,对象的地址与变量存在一起。
如果 xxx 是变量,变量 xxx 栈中存的是什么,变量 a 的栈中就存什么。

1.2 JavaScript 中的垃圾回收机制(GC)

(1) 垃圾回收相关概念

① 什么是垃圾

没用被引用的对象就是垃圾

② 什么是垃圾回收。

销毁垃圾对象,释放内存,就是垃圾回收。

C / C++: 手动回收垃圾。

JavaScript / Java / Python / php 等: 自动回收垃圾。

③ 变量的生命周期(何时会被回收)
全局变量: 全局代码执行完毕,变量会被销毁。
局部变量:本作用域代码执行完毕,局部变量被销毁。
④ 垃圾没有及时回收的后果
垃圾对象不及时回收,常驻内存,导致内存容量不够,造成内存泄漏。

⑤ 垃圾回收的常见算法

- 引用计数  (老版本 IE)
- 标记清除	(大部分现代浏览器)

(2) 引用计数

① 原理
- 每个对象建立一个引用标记,用来标记被引用的次数
- 对象没增加一次引用,引用标记就 +1
- 对象被减少一次被引用,引用标记就 -1
- 如果对象的引用标记变为 0,被认为是垃圾对象,立即被清除(回收)
② 优缺点:
- 优点: 垃圾回收非常及时,引用计数一旦为0立即被回收。
- 缺点: 对象互相引用导致引用计数永远无法为0,不能被回收,造成内存泄漏。

(3) 标记清除

原理
定时进行标记清除,每次标记清除分为两个阶段:
- 标记阶段:	从根对象开始,一层一层地向下访问对象,能访问到的对象,添加一个标记,称之为可到达对象,不能访问到的成为不可到达对象。
- 清除阶段: 遍历内存中所有的对象,如果没有可到达的标记,就被认为是垃圾对象,被回收。
优缺点
- 优点: 不会造成内存泄漏
- 缺点: 需要深层递归,效率较低,垃圾对象不如引用计算算法回收得及时

2 函数高级

2.1 执行上下文和执行栈

(1) 执行上下文

① 全局执行上下文
1.window 确定为全局执行上下文对象。
2. 对全局作用域下的数据进行预处理:
	① 所有 var 声明的全局变量,都作为全局执行上下文对象的属性,默认赋值 undefined。
	② 所有 function 关键字形式声明的全局函数,都作为全局执行上下文对象的方法,有值。
	③ 将 this 的值确定为 window(全局执行上下文对象)
3. 执行全局代码。
② 函数内的执行上下文

函数每调用一次,就创建新的执行上下文对象。

1. 当函数调用的时候,先创建该函数的执行上下文对象。
2. 对函数内的数据进行预处理:
	① 给形参进行赋值,把形参作为执行上下文对象的属性。
	② 给 arguments 赋值,把 arguments 作为执行上下文对象的属性。
	③ 把函数内 var 声明的变量,作为执行山下文对象的属性,值 undefined。
	④ 把函数内 function 关键字形式声明的函数,作为执行上下文对象的方法,有值。
	⑤ 将 this 的值确定为调用该函数的对象
3. 执行函数体内的代码

(2) 执行栈

执行栈是用来存储执行上下文对象的存储结构,是具有先进后出(后进先出)特性的栈结构。

把数据放入栈结构的过程称为压栈,销毁栈中的某个数据称为出栈.

哪个执行上下文对象先创建,就哪个先压栈。先压栈的往往最后一个出栈。

1. 代码执行之前,先创建执行栈结构
2. 当确定了全局执行上下文对象之后,就将全局执行上下文对象压栈,开始执行该上下文对象中的语句。
3. 当调用某个函数的时候,创建该函数的执行上下文对象,执行上下文对象压栈,执行函数体内语句。
4. 当函数调用结束,该函数的执行上下文对象出栈
5. 等到所有的函数执行上下文对象都出栈了,全局执行上下文对象才出栈,意味着整个js脚本执行结束。

(3) 作用域和执行上下文的关系

- 区别:作用域是静态的,函数声明的时候就确定了; 执行上下文对象是函数调用的时候动态创建的,没调用一次就创建一次。
- 联系:执行上下文对象也是有作用域的,全局执行上下文对象->全局作用域; 函数内的执行上下文对象->本函数。
       全局变量都是全局执行上下文对象的属性。
       局部变量都是该函数执行上下文对象的属性。

2.2 闭包

(1) 什么是闭包?

可以访问其他作用域中的数据,这种现象就是闭包

(2) 如何产生闭包

1. 函数 A 中创建函数 B
2. 函数 B 中使用函数 A 作用域中的数据
3. 在函数 A 中,要把函数 B 返回,或者以其他方式引用(如函数B作为回调函数) (切记:函数B不在函数A中调用)
引用函数 B 的方式:
1. 把函数 B 返回
2. 函数B被其他变量或属性引用
3. 函数B作为回调函数(事件或定时器)
function A(){  
  var a = 1,b = 2;  

  function B(){  
    return a+b;  
  }  
  return B;  
}  
var fn = A(); // 返回的是函数B

fn();

(3) 闭包和作用域

1. 调用 fn(). fn 和 B 都是函数,调用 fn, 执行的是函数 B 里的语句
2. 函数 B 使用上层作用域的变量 abab 在函数 A 的作用域中)
3. 最终现象: 函数 fn 中可以使用 函数 A 作用域中的数据

作用域只与函数声明的位置有关系。

(4) 闭包和垃圾回收

1. 当函数 A 调用结束之后,函数 A 的局部变量应该被回收。
2. 但是变量 fn 引用函数 B,函数 B 的语句又引用了 ab,导致函数和两个值都不会被回收

闭包可以延长函数内局部变量的声明周期!

(5) 闭包的缺点

闭包会造成数据长时间在内存中,具有内存泄漏的风险!

闭包注意事项:
1. 有闭包以外的其他方案解决,就使用其他方案。
2. 减少引用次数,必要时断开引用(给变量重新赋值)

(6) 闭包的应用

1. 循环给多个元素监听事件,需要用闭包存储一些数据。
2. JS 模块化,使用闭包暴露数据

3 对象高级

3.1 创建对象的方式

① Object 构造函数

new Object();

② 直接量/字面量方式

{};

③ 工厂模式

// 定义工厂函数
function factory(name, age) {
    if (age > 200) {
        age = 200;
    }
    return {
        name: name,
        age: age
    }
}

var obj1 = factory('朦朦', 18);
var obj2 = factory('大悲', 398);

④ 自定义构造函数

function User() {
    
}

new User();

⑤ 自定义构造函数和原型结合

function User(name, age) {
    this.name = name;
    this.age = age;
}
User.prototype.getInfo = function() {
    
}

3.2 原型链总结

__proto__		该属性指向自身的原型对象
prototype		该属性指向实例的原型,(该属性只有构造函数才有)
constructor		指向以该对象为原型的对象的构造函数

3.3 面向对象继承

① 面向对象语言的继承规则

一般,面向对象语言继承都是 类和类之间继承。

② JS 中继承关系的特点

JS中,继承是通过原型链继承,继承发生在对象(实例)和对象(实例)之间。

instanceof 判断一个对象是否是某个构造函数的实例; 第二个操作写一个构造函数,该构造函数是第一个操作数的构造函数或者第一个操作数原型链上的某个对象的构造函数,都成立,返回 true

③ 实现JS中构造函数和构造函数之间继承

// 定义父类
function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.say = function() {}

// 定义子类
function Girl(name, age, address) {
    // 给从父类继承过来的属性赋值
    Person.call(this, name, age);
    // 给自身实例的属性赋值
    this.address = address;
}
// 设置 Gril 的实例的原型是 Person 的实例
Girl.prototype = new Person();
// 设置 Girl 的实例的原型上 constructor 属性,指向 Girl
Girl.prototype.constructor = Girl;


// 创建 Girl 的实例
var g = new Girl();
对象 g 的原型链:

对象g -> Girl.protype(Person的实例) -> Person.protype(Object的实例) -> Object.protype

3.4 对象属性的高级特性

① 数据属性

数据属性具有如下 4 个特性:

configurable 可配置性,决定是否可以用 delete 运算符删除该属性, 默认是 true。

enumerable 可遍历性,决定是否可以用 for in 遍历出该属性,默认是 true。

writeable 可写性,决定是否可以修改该属性的值。

value 值,存放该属性的值。

② 访问器属性

访问器属性是不具备数据值的属性,设置属性和访问属性会调用 set 和 get 两个函数,访问器属性具有如下特性:

configurable 可配置性,决定是否可以用 delete 运算符删除该属性, 默认是 true。

enumerable 可遍历性,决定是否可以用 for in 遍历出该属性,默认是 true。

get: 访问该属性时调用的函数,该函数的返回值就是属性值。

set: 给该属性设置值的时候调用的函数,可以接受新设置的值作为参数。

③ 设置属性的特性

Object.defineProperty(对象, 属性名, {})
Object.defineProperties(对象, {})
// 设置数据属性的特性
// 如果属性已经存在就修改该属性特性;如果属性不存在就添加新属性
Object.definedProperty(对象, 属性名, {
    configurable: true,
    enumerable: true,
    writeable: true,
    value: ''
})
 // 给 obj 添加一个访问器属性
Object.defineProperty(obj, 'age', {
    configurable: true,
    enumerable: true,
    get: function() {
        return this._age;
    },
    set: function(value) {
        if (value >= 0 && value <= 200) {
            this._age = value;
        }
    }
});
// 同时设置多个属性的特性
// 添加属性并设置特性
Object.defineProperties(obj, {
    name: {
        configurable: true,
        enumerable: false,
        writable: true,
        value: '朦朦'
    },
    age: {
        value: 100
    },
    address: {
        configurable: false,
        enumerable: false,
        get: function() {

        },
        set: function() {

        }
    }
});

④ 获取对象中一个属性的特性

Object.getOwnPropertyDescriptor(对象, '属性名');

3.5 安全的类型检测

① typeof

只能查看 number、string、boolean、function,其他都返回 object

② instanceof

可以判断一个对象是否是某个构造函数的实例;

对象自己的构造函数返回 true,原型链上的对象的构造函数也会返回 true。

③ 对象的 constructor 属性

有些对象可以通过该属性返回自己的构造函数。

但是,作为其他对象原型的对象, 返回的是以他为原型的那个对象的构造函数。

④ 安全的类型检测

// 封装获取对象类型的函数
function getSafeType(obj) {
    var typeStr = Object.prototype.toString.call(obj)
    return typeStr.slice(8, typeStr.length - 1);
}

4 单线程和事件轮询机制

4.1 进程和线程

1. 什么是进程?
系统进行资源调度和分配的基本单位。

2. 什么是线程?
系统进行运算的最小调度单位,是进程的实际运作单位。


3. 进程和线程:
- 一个进程中至少有一个线程,称为主线程。
- 一个进程可以有多个线程,多线程运行。
- 同一个进程中线程,可以共享数据。
- 不同进程之间是不能资源共享。

4.2 JS 单线程运行

为什么 JS 选择单线程运行?
JS 设计之初,主要作用就是操作 DOM 完成页面效果,如果是多线程执行无法解决页面渲染同步问题、

4.3 同步代码和异步代码

同步代码:
也叫同步任务,安装顺序依次执行,后面的代码需等到前面代码执行完毕才执行

异步代码:
也叫异步任务,当开始异步任务之后,会监听异步代码的满足条件,不影响同步代码继续执行;但异步任务满足条件且同步代码都执行完毕,异步代码才行。

哪些是异步代码?
1. 定时器的回调函数
2. DOM 事件的回调函数
3. Ajax 的回调函数
4. Promise 的回调函数

4.4 事件轮询机制

执行栈(调用栈): 所有代码要执行必须进入执行栈。

管理模块: 包括定时器的管理模块、DOM事件的管理模块、Ajax的管理模块等, 检测异步任务什么时候满足条件(定时器事件到、事件触发、ajax获取响应),满足条件之后,将回调函数放入回调队列。

回调队列: 队列是一种先进先出的数据结构,回调队列用于存放待执行的回调函数。

事件轮询模块: 负责检测执行栈什么时候空闲,一旦有空闲,就取出回调队列中最前面的回调函数,放入执行栈执行,