JavaScript 基础

308 阅读35分钟

第一章 JavaScript语言

1. JavaScript语言诞生

JavaScript语言可以概括为“函数式编程+面向对象编程”。

  1. 动态类型
// JavaScript声明变量
var obj = 100;
obj = true;

// C++声明变量
int num = 100;
float num2 = 3600;
string str = 'hello world';

JavaScript声明变量时并不关心变量的类型,可以给已声明的变量赋予任意的值。而C++是静态类型语言,声明变量前需要指出变量的数据类型,且不能随意更改。

由于JavaScript的动态类型语言的特点,导致其不能在运行前被编译成更加迅速的低级语言代码,即机器码。而随着JavaScript引擎的发展,出现了Just-In-Time Compilation(JIT)技术,简称运行时编译。该项技术使JavaScript脚本在运行时被编译为低级的机器语言,从而使其执行速度变得更快。

2. V8引擎工作原理

V8引擎是一个接收JavaScript代码,编译并执行的C++程序。编译后的代码可以在多种操作系统上运行。

V8引擎主要包括编译执行JS代码、处理调用栈、内存分配 、垃圾回收等。

解析执行JS

image.png

1️⃣:parser解析器会将JavaScript代码转化为抽象语法树AST

没有被调用的函数不会被转化为AST

2️⃣:lgnition解释器基于AST产出字节码,并运行字节码收集类型反馈。字节码与平台无关,可以在不同的操作系统上运行

3️⃣:V8引擎可以检测行为是否经常发生,以及该行为使用的数据类型等。为了使代码运行的更快,字节码和反馈数据会一起被发送到TurboFan优化编译器

只被执行一次的函数,字节码直接被解释执行,不会经过优化编译器(优化编译器需要很多类型数据信息,只被调用一次的函数无法获取更多的信息)

4️⃣:TurboFan优化编译器基于字节码和反馈数据做出某些假设,并产出高度优化的机器代码(内联缓存技术)

被多次调用的函数会被标记为热点函数,TurboFan会将其转化为优化的机器码

5️⃣:当优化编译器的假设被证明是不正确的,优化编译器则会取消优化回到解释器

调用栈

调用栈是JS引擎追踪函数执行流程的一种机制,当执行环境中调用了多个函数时,通过这种机制,可以追踪到哪个函数正在执行,执行的函数体又调用了哪个函数。调用栈采用了先进后出的数据结构管理函数。

当函数被声明时不会进入调用栈,只有函数被执行时才会入栈,函数执行完毕后会出栈。

函数被调用时入栈 image.png 函数调用完毕后出栈 image.png

当函数调用中出现递归时,如果没有标明递归结束条件,很容易出现堆栈溢出,造成程序卡死。

image.png image.png

内存分配

V8引擎的内存分配策略是一项重要的技术,它直接影响到应用程序的性能和内存占用。V8引擎采用了两种内存分配策略:线程本地分配(Thread-Local Allocation)和预分配(Pretenuring)

  1. 线程本地分配
  • 原理:线程本地分配的思想是为每个线程维护一个私有的内存池(简称TLAB),线程可以在其私有内存池中快速分配和释放内存,避免了锁竞争内存分配器的共享,从而提高了分配和释放内存的性能。每个线程可以独立地管理自己的内存池,而不会影响其他线程的内存分配和释放

  • 使用:需要分配内存时,V8会尝试从线程的私有内存池中分配内存。如果线程的内存池已满,V8会从堆中分配一块大内存,然后将其划分为多个TLAB,分配给每个线程

  • 优点:可以提高内存分配和释放的性能,减少锁竞争和内存分配器的共享,从而提高了应用程序的性能和可伸缩性

  • 缺点:内存碎片化和内存浪费等问题

  1. 预分配
  • 原理:在对象的生命周期中,为其分配足够的内存空间,以避免不必要的内存分配和释放。V8为了提高内存分配和释放的性能,采用了“内存池”技术。内存池是一块预先分配的连续内存空间,用于存储一组相同类型的对象。当需要分配对象时,V8会从内存池中分配一块足够大小的内存空间,并将其分配给对象

  • 优点:可以减少不必要的内存分配和释放,从而提高了应用程序的性能和可伸缩性

  • 缺点:可能会导致内存浪费和内存碎片化等问题

垃圾回收机制

V8的垃圾回收机制主要包括新生代垃圾回收、老生代垃圾回收和增量式垃圾回收。

  1. 新生代垃圾回收

新生代是指刚刚被创建的JavaScript对象。新生代的对象分配在一块称为From空间的内存区域中。当From空间满了之后,V8会触发新生代垃圾回收。在这个过程中,V8会将From空间中的存储对象复制到To空间的内存区域中,并清空From空间。这个过程称为Scavenge (清除)

优点是简单、快速。缺点是需要复制大量的对象造成内存的浪费

  1. 老生代垃圾回收

老生代是指已经存活一段时间的JavaScript对象。老生代的对象分配在一块称为Oldspace的内存区域中。由于老生代中的对象较大,且存活时间较长,因此老生代的垃圾回收比新生代更为复杂。

在老生代中,V8采用了标记-清除 (Mark-Sweep) 和标记-整理 (Mark-Compact) 两种垃圾回收算法。

  • 标记-清除算法:V8首先会遍历堆中所有的对象的引用,并将引用的对象标记为存活的对象。标记完成后,V8会清除所有未标记的对象,并释放它们所占用的内存空间。由于清除过程需要遍历整个堆,因此它的性能较低,并且可能会导致堆内存碎片化

  • 标记-整理算法:V8同样会先标记所有存活的对象。然后将所有存活的对象移动到堆的一端,并清空移动后的另一端的内存空间。整理过程可以有效地解决堆内存碎片化的问题,由于涉及到移动对象,所以整理过程的性能比较低

  1. 增量式垃圾回收

V8将垃圾回收过程分成多个阶段,包括标记、清除和整理等阶段,每个阶段执行一小部分的垃圾回收操作,并在其中间暂停,以便让JavaScript代码执行。这样可以避免长时间的垃圾回收阻塞了JS脚本的执行,提高了应用程序的响应速度。

第二章 数据类型

1. 分类

数据类型共有7个,分别为number,string,boolean,null,undefined,symbol,object

  • 基础类型范畴:Number、String、 Boolean、Null、 Undefined、 Symbol、 BigInt(数值型,任意长度的整数)
  • 引用类型范畴:Object、 Array、Date、Error、 Function、 RegExp

2. 区别

对比基本数据类型引用数据类型
占用空间
内存大小固定不固定
存储位置
访问方式通过值通过指针
a=ba,b互不影响a,b指向相同的实体

基本数据类型占用空间小,内存大小固定,通过值访问,使用频繁,保存在栈内存中。

引用数据类型占用空间大,内存不固定,存储在堆内存中。

引用数据类型在栈中存储了指针,指针指向堆中该实体的实际地址,当解释器寻找引用值时,会首先检索其在栈的存储的指针,获取地址,根据地址从堆中获得实体。

let a = 1; 
let b = a; // 在栈中开辟了一块新的内存空间并赋值为1。 
b = 2; // b变为2并不会影响到a的值。
console.log(a, b);  // 输出1 2 

let c = {num: 1}; 
let d = c; // 这里d其实是指向了堆中同一个地址。 
d.num = 2; // 所以这里d改变了,c也会跟着变。 
console.log(c.num, d.num); // 输出 2 2

3. 类数组对象

拥有length属性和若干索引属性的对象,和数组类似,但是不能调用数组的方法。

常见的类数组对象有函数获取所有参数的arguments和DOM方法的返回结果,如document. getElementsByClassName等。

类数组转为数组的方法

  • Array.prototype.slice.call(arrayLike)
  • Array.prototype.concat.apply([], arrayLike)
  • Array.from(arrayLike)
  • [...arguments]

4. Number数据存储

JavaScript采用“遵循 IEEE 754 标准的双精度64位格式”表示数字。

  • sign bit符号:正负号
  • exponent指数:次方数
  • mantissa尾数:精确度

0.1 + 0.2 !== 0.3

JS中0.1 对应的二进制是 1 * 2^-4 * 1.1001100110011……,当完整的0.1存储时发生了精度丢失,把两个精度丢失的数字进行运算,其精度损失会叠加

解决办法:toFixed(n)、乘以 100(或更大的数字)再除以100

第二章 数据类型判断

1. typeof

可以判断number、string、boolean、undefined、symbol、function6种数据结构,其他数据结构都会返回'object'

返回值:字符串,小写类型名称

// 可判断类型
typeof 5 'number'
typeof '5' 'string'
typeof true 'boolean'
typeof undefined 'undefined'
typeof function(){} 'function'
// 不可判断类型
typeof null 'object'
typeof [1,2,3] 'object'
typeof new Date() 'object'
typeof /abc/ 'object'

2. instanceof

只能判断引用数据类型和自定义引用数据类型

返回值:true或false

底层原理:左边对象是否为右边类的实例(检测构造函数的 prototype 是否出现在实例对象的原型链上)

console.log(newNumber(1) instanceof Number); //true 
console.log(new String("a") instanceof String); //true 
console.log(new Boolean(false) instanceof Boolean); //true
console.log(new Date() instanceof Date); //true 
console.log(new Array(10) instanceof Array); //true
console.log(User instanceof Function); //true 
console.log(new Error("自定义错误") instanceof Error); //true 
console.log(new RegExp("[0-9]?") instanceof RegExp); //true 
console.log(/[0-9]?/ instanceofRegExp); //true

手动封装一个instanceof方法

function myInstanceof(left, right) {
  let proto = Object.getPrototypeOf(left); // 获取对象的原型
  let prototype = right.prototype// 获取构造函数的 prototype

  // 循环判断构造函数的 prototype是否在对象的原型链上
  while (true) {
    if (!proto) return falseif (proto === prototype) return true;

    proto = Object.getPrototypeOf(proto);
  }
}

3. Object.prototype.toString.call()

可以判断基本数据类型和引用数据类型,不能判断自定义对象的数据类型

返回值:'[object xxx]'

Object.prototype.toString.call([]) //'[object Array]'
Object.prototype.toString.call(new Number(1)) //'[object Number]'

4. Array.isArray

ES5提出的检测数组的方法

返回值:true或false

Array.isArray([]) // true

手动封装数据类型判断函数

function getType(value){
    if(value === null){
        return value+'';
    }
    if(typeof value !== 'object'){
        return typeof value;
    }
    const type = Object.prototype.toString.call(value).split(' ')[1].split('');
    type.pop(); 
    return type.join('');
}

第三章 数据类型转换

1. 转布尔类型

方法:Boolean()、new Boolean() 

undefined、null、false、 0、+0、-0、NaN、"" 转化结果为false,其余为true

2. 转字符串类型

方法:全局方法 String() 、new String()、+ ''(隐式转化)

  • 基本数据类型则将该值转换为String类型
  • Symbol 类型只允许显式强制类型转换,使用隐式强制类型转换会产生错误
  • 对象类型先调用 toString(),再调用valueOf()方法,最后都不行则报错Uncaught TypeError: Cannot convert object to primitive value

3. 转数值类型

方法:全局方法 Number() 、new Number()、parseInt()、parseFloat

1)Number

  • 基本数据类型会将该值转换成Number类型
  • undefined 转换为 NaN
undefined + 1 = NaN
  • Null 转换为 0
null + 1 = 1
  • Boolean 类型的值,true 转换为 1,false 转换为 0
  • String 类型的值转换如同使用 Number() 函数进行转换,如果包含非数字值则转换为 NaN,空字符串为 0
  • BigInt类型转换为Number的时候会精度丢失
  • Symbol 值不能转换
  • Object类型会首先调用 valueOf() 方法,再调用 toString(),均不成功会报错Uncaught TypeError: Cannot convert object to primitive value

2)parseInt

parseInt(string, radix) 解析一个字符串并返回指定基数的十进制整数, radix 是2-36之间的整数,表示被解析字符串的基数

3)parseFloat

该函数指定字符串中的首个字符是否是数字,如果是则对字符串进行解析,直到到达数字的末端为止,然后以数字返回该数字,而不是作为字符串

第四章 函数

1. 函数的定义

1)function声明函数

函数声明有预解析,函数声明的优先级高于变量

function getSum(){} function (){}//匿名函数 ()=>{}

2)函数表达式(字面量)

var sum=function(){} let sum=()=>{}

3)构造函数

const sum = new Function('a', 'b' , 'return a + b')

构造函数是一个函数表达式,这种方式会导致解析两次代码,第一次解析常规的JavaScript代码,第二次解析传入构造函数的字符串,影响性能

2. 函数内this指向

1)全局上下文

this指向全局对象

2)函数上下文

  • 对象的方法调用:this 指向该对象
  • 普通函数调用:非严格模式指向全局对象,严格模式下指向undefined
  • call、apply、bind 调用:this 指向绑定的对象
  • 构造函数new调用:this 指向新的对象
  • 箭头函数调用:this指向全局对象

其中call、apply、bind调用传参时判断:

  • 参数为空或为null、undefined,this指向window;
  • 参数为对象类型,this指向对象;
  • 参数为string、number、boolean类型,call和apply内部会调用其相应的构造器String、Numer、Boolean将其转换为相应的实例对象,this指向这个实例对象
function foo() { 
    console.log(this); 
};

foo.apply('我是apply改变的this值');//我是apply改变的this值 
foo.call('我是call改变的this值');//我是call改变的this值

3)类上下文

this指向类

4)原型链调用

this指向调用方法的对象

第五章 call、apply、bind

1. call和apply

对象.call(新this对象,实参1,实参2,实参3.....) 
对象.apply(新this对象,[实参1,实参2,实参3.....])

底层操作:

  • 第一步改变内部this 的指向;
  • 第二步执行函数;

适用场景:

  • 劫持对象的方法,改变this指向
var foo = { 
    name:"张三", 
    logName:function(){ 
        console.log(this.name); 
    } 
} 
var bar={ name:"李四" };
foo.logName.call(bar); //李四 实质是call改变了foo的this指向为bar,并调用该函数
  • 两个函数实现继承
function Animal(name){  
    this.name = name;  
    this.showName = function(){  
        console.log(this.name);  
    }  
}  
function Cat(name){  
    Animal.call(this, name);  
}  
var cat = new Cat("Black Cat");  
cat.showName(); //Black Cat
  • 为类数组(arguments和nodeList)添加数组方法push,pop
(function(){
  Array.prototype.push.call(arguments,'王五');
  console.log(arguments);//['张三','李四','王五']
})('张三','李四')
  • 合并数组
//将arr2合并到了arr1中
let arr1=[1,2,3]; 
let arr2=[4,5,6];  
Array.prototype.push.apply(arr1,arr2); 
  • 求数组最大值
Math.max.apply(null,arr)
  • 判断字符类型
Object.prototype.toString.call({})

手动封装call方法

Function.prototype.myCall = function (context= window) {
    context.fn = this; // this指向fn函数
    const args = Array.from(arguments).slice(1);
    const result = args.length > 0 ? context.fn(...args) : context.fn();
    delete context.fn;
    return result;
};

function fn(a, b) {
    console.log("this", this, "参数", a, b);
}

let Fn1 = fn.call({ name: "胖胖", age: 25 }, 10, 20);
let Fn2 = fn.myCall({ name: "胖胖", age: 25 }, 10, 20);

image.png

手动封装apply方法

Function.prototype.myApply = function (context = window, arr) {
    context.fn = this; // this指向fn函数
    let result = arr.length > 0 ? context.fn(...arr) : context.fn();
    delete context.fn;
    return result;
};

function fn(a, b) {
    console.log("this", this, "参数", a, b);
}

let Fn1 = fn.apply({ name: "胖胖", age: 25 }, [10,20]);
let Fn2 = fn.myApply({ name: "胖胖", age: 25 }, [10,20]);

image.png

2. bind

1)底层操作

  • 创建一个新函数
  • 将新函数的this值指向新对象
  • 将新函数的参数与绑定函数的参数合并
  • 调用绑定函数,将绑定函数的结果返回给新函数

2)bind特点

  • bind不会立即执行
  • bind只是创建了一个新函数,改变了this指向,合并了bind函数和新函数的参数
  • bind会返回一个新的函数
var foo = { 
    name: "张三", 
    logName: function() { 
        setTimeOut(function(){
            console.log(this.name); 
        },100)
    } 
} 
foo.logName(); // undefind this指向window

// ES5之前的方式,创建变量保存外部this
logName: function() {
    var _this = this;
    setTimeOut(function(){
        console.log(_this.name); 
    },100)
} 
// ES5的方式
foo.logName.bind(foo); // 张三,创建一个新函数,新函数内部的this指向foo

手动封装bind方法

let obj = {
    name: "小猪课堂",
    age: 20
};

Function.prototype.myBind = function (context) {
    // 当前函数
    const _this = this;
    // 将参数列表转化为数组,除去第一个参数外
    let args = Array.from(arguments).slice(1);
    // 创建新函数
    let fn = function () {
      //被new调用,this指向fn实例
      if(this instanceof fn){
          return _this.apply(this, args.concat(Array.from(arguments)));
      }else{
          return _this.apply(context || window, args.concat(Array.from(arguments)));
      }
    }
    // 维护fn的原型
    let temp = function () {};
    temp.prototype = _this.prototype;
    // 继承temp原型
    fn.prototype = new temp();
    // 返回新函数
    return fn;
};

// 声明一个函数
function fn(a, b, c) {
    console.log("函数内部this指向:", this,"参数列表:", a, b, c);
}

let newFn = fn.myBind(obj, 10, 20);
let newFn1 = fn.bind(obj, 10, 20);

newFn("myBind");//函数内部this指向: {name: '小猪课堂', age: 20} 参数列表: 10 20 myBind
newFn1("bind");//函数内部this指向: {name: '小猪课堂', age: 20} 参数列表: 10 20 bind

new newFn("myBind构造函数");//  函数内部this指向: fn {} 参数列表: 10 20 myBind构造函数
new newFn1("bind构造函数"); // 函数内部this指向: fn {} 参数列表: 10 20 bind构造函数

第六章 节流和防抖

含义
节流在一定时间内,多次触发同一个事件,只执行第一次操作
防抖在一定时间内,多次触发同一个事件,只执行最后一次操作

1. 防抖

防止在短时间内多次触发函数,触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内再次触发函数,则会重新计算等待时间,触发函数时会取消之前的延时调用方法

实现方式:定时器

函数:loadsh库的debounce

2. 节流

节省操作,在n秒内连续触发事件,但事件只执行一次函数,节流会稀释函数的执行频率,每次触发事件时都判断当前是否有等待执行的延时函数

实现方式:boolean变量控制

函数:loadsh库的throttle

第七章 作用域

作用域是当前的执行上下文,值和表达式在其中“可见”或可被访问

1. 作用域划分

1)静态作用域

静态意味着它与代码的位置有关,与执行代码时的环境无关,JavaScript 采用的是静态作用域

2)动态作用域

动态即运行时,代码执行时确定的作用域

2. 静态作用域

  • 全局作用域:脚本模式运行所有代码的默认作用域
  • 函数作用域:由函数创建的作用域
  • 块级作用域:用{}创建出来的作用域
  • 模块作用域:模块模式中运行代码的作用域

1)全局作用域

JavaScript 变量最外层的作用域,全局作用域中的变量称为全局变量,可以在任何作用域内访问,全局变量分为两种:全局声明变量和全局对象

  • 全局声明变量:普通变量,在全局作用域顶部由const、let 和 class 声明的变量
  • 全局对象:存储在全局对象中的属性,在全局作用域顶部由 var 和 function 声明后创建的变量,全局对象可以通过 globalThis 和 window 访问

2)函数作用域

函数内声明的变量,只能在函数作用域范围内访问

3)块级作用域

ES6新增的作用域,let、const、class均支持块级作用域

4)模块作用域

每个 ECMAScript 模块(ES6 Modules)都有自己的作用域,因此在顶级模块中声明的变量不是全局的

3. 作用域链

作用域是相对于变量而言,由于代码结构复杂,存在多级作用域,要想查找变量的作用域,就需要一级级查看,把查看变量作用域的过程称为作用域链。可以确定变量可访问的范围。

4. 闭包

内部函数有权访问外部函数作用域内的变量。闭包除了和作用域有关,也和垃圾回收的机制有关。

正常的垃圾回收过程是:当一个函数执行时会给它分配空间,当执行结束后会把空间收回,但是当一个局部变量被另一个函数使用,那么它就不会被释放,也就不会被垃圾回收,就是形成了闭包。

function fn(){
    var a = 5; // 未释放
    return function(){
        a++;
        console.log(a);
    }
}

var f1 = fn();
f1(); // 6
f1(); // 7
f1(); // 8

var f2 = fn();
f2(); // 6
var f3 = fn();
f3(); // 6

闭包的执行绕过了作用域的监管机制,从外部也能获取到内部作用域的信息

  • 同一个闭包内的变量共用
  • 不同闭包内的变量不共用,是一个新变量

闭包作用:

  • 缓存作用域变量,延长变量的生命周期
  • 私有化数据,避免全局变量的污染
  • 利于闭包实现防抖、节流、选项卡等

闭包缺点:

当变量一直保存在内存中,有可能导致内存泄露,需要及时把变量置空(变量 = null)

第八章 构造函数

// 构造函数
function People(name){
  this.name = name
}
// 实例化对象
const p1 = new People('randy');

1. new的作用

1)new执行动作

第一步:创建一个空对象,做为将要返回的对象实例

第二步:将空对象的原型,指向构造函数的prototype属性

第三步:将空对象赋值给构造函数内的this关键字

第四步:开始执行构造函数内部的代码

2)new的作用

  • 创建一个空新对象
  • 将构造函数内部的 this 指向空对象
  • 执行构造函数内部的代码,初始化对象的属性和方法
  • 返回新对象

2. 构造函数返回值

1)如果构造函数无return或者return基本数据类型,则返回值为构造函数的实例

function Student(){
    return '111';
}
const p1 = new Student();

console.log(p1 instanceOf Student) // true

2)如果构造函数return引用数据类型,则返回值为该数据

function Student(){
    return [1,2,3,4];
}
const p1 = new Student(); // p1=[1,2,3,4]

console.log(p1 instanceOf Student) // false
console.log(p1 instanceOf Array) // true

3. 构造函数属性

1)静态属性

静态属性是构造函数的私有属性,只能通过“构造函数.属性名 ”的方式访问,实例对象不能访问

function People(name){ };
// 静态属性
People.age= 24const p1 = new People('小明');

console.log(p1.age) // undefined
console.log(People.age); // 24

2)实例属性

实例属性是实例对象的私有属性,只能通过“实例对象.属性名”的方式访问,构造函数不能访问

function Foo(name) {
  this.name = name;
}

const f1 = new Foo("f1");

// 给实例对象添加属性方法
f1.getName = function() {
  console.log('这里是实例属性');
}

console.log(Foo.getName); // undefined 构造函数本身没有这个方法

console.log(f1.getName); // ƒ () { console.log("这里是实例属性"); }

3)原型属性

原型属性在构造函数的原型对象上,构造函数和实例对象都可以访问原型属性,通过“构造函数.prototype.属性名”或者“实例对象.属性名”访问

function Foo(name) {
  this.name = name;
}
// 在构造函数的原型上添加属性
Foo.prototype.getName = function () {
  console.log("这里是原型属性");
};

const f1 = new Foo("f1");

console.log(Foo.prototype.getName); // ƒ () { console.log("这里是原型属性"); }
console.log(f1.getName); // ƒ () { console.log("这里是原型属性"); }

第九章 原型

几乎每个对象(实例)的内部都有一个__proto__属性指向对象的原型,在控制台中__proto__表现为[[Prototype]]

  • 每个构造函数都有一个原型对象
  • 每个实例对象都有一个__proto__属性

1. 关系网

  • 构造函数的prototype指向原型对象
  • 原型对象的constructor指向构造函数
  • 实例对象的__proto__属性指向原型对象

  • 构造函数包含有prototype和__proto__
  • 实例对象只有__proto__
  • 实例对象的__proto__ = 构造函数的prototype

2. 创建对象

1)基本方式

  • Object 构造函数
let Obj=new Object() Obj.name='张三'
  • 对象字面量
let obj={'name':'张三'}
  • Object.create()
let person = {
  name: "Lucy",
  sayName() {
    console.log(this.name)
  }
}

let person1 = Object.create(person);
  • class
class Person {
  constructor(name) {
    this.name = name;
  }
  sayName() {
    console.log(this.name)
  }
}

const person1 = new Person("Lucy");

2)继承模式方式

  • 工厂模式
function createPerson(name){
     var o = new Object();
     o.name = name;
     return o; 
}
var person1 = createPerson('张三');

优点:创建多个类似对象 缺点:对象标识局限性

  • 构造函数模式
function Person(name) {
    this.name = name;
    this.sayName = function() {
        console.log(this.name)
    }
}

let person1 = new Person("Lucy");
let person2 = new Person("Joe");

优点:自定义对象标识 缺点:构造函数内部方法在每个实例上都创建一遍,内存浪费

  • 原型模式
function Person() {};
Person.prototype.name = "Lucy";
Person.prototype.sayName = function() {
  console.log(this.name)
}

let person1 = new Person();

优点:实例共享的属性和方法放到原型链上,减少内存使用 缺点:实例属性和方法一致,缺乏独特性

  • 组合模式

原型模式和构造函数模式的结合

function Person(name) {
  this.name = name;
}

Person.prototype = {
  constructor: Person,
  sayName: function() {
    console.log(this.name)
  }
};

let person1 = new Person("Lucy");
let person2 = new Person("Joe");

person1.sayName(); //"Lucy"
person1.constructor === Person; // true

person2.sayName(); //"Joe"
person2.constructor === Person; // true

优点:既有共有也有私有属性和方法 缺点:封装性不太好

3. 操作原型对象

1)获取原型对象

  • 实例.__proto__
  • 构造函数.prototype
  • Object.getPrototypeOf(实例)

2)修改原型对象

Object.setPrototypeOf(实例,新的原型对象)

3)判断原型对象

原型对象.isPrototypeOf(实例)

4)获取根对象

任何对象都继承自Object.prototype 

const arr = [1,2,3,4,5] 
const 根对象 = arr.__proto__.__proto__  // arr.__proto__ = Array.prototype

4. 原型链

原型链的根本为继承,原型链就是通过继承形成的一个关系图,其核心就是通过__proto__属性将若干个对象连接起来。

原型链上查找方法:

  • 首先看实例对象上是否存在该方法,如果存在则执行;
  • 如果不存在,就在实力对象的原型对象即构造函数原型对象prototype上查找;
  • 如果不存在,就在原型对象的原型对象上查找,直到Object原型对象prototype为止;
  • 如果不存在,则会报错;

第十章 继承

JS是一门弱类型动态语言,封装和继承是他的两大特性。

继承就是一个对象可以通过委托访问另一个对象的属性和方法。

最常用的继承方式是原型链继承、原型继承、构造继承、拷贝继承、ES6的extend继承

1. 原型链继承 (单个属性或方法)

公共的属性和方法放到构造函数的原型对象上,所有实例都可以共享

缺点:代码冗余、容易被实例对象误改公共属性

// 构造函数
function Animal (name) { 
    this.name = name; 
} 
// 添加原型方法
Animal.prototype.eat = function(food) { 
    console.log(this.name + '正在吃:' + food); 
};
Animal.prototype.sleep = function(){
    console.log(this.name + '正在睡觉!'); 
};

// 创建实例
var cat = new Animal('cat');

console.log(cat.name); //cat 
console.log(cat.eat('fish')); //cat正在吃:fish
console.log(cat.sleep());//cat正在睡觉!
console.log(cat instanceof Animal); //true 

2. 原型链继承 (修改整个原型)

修改构造函数的原型对象为一个新的对象

缺点:代码冗余,内存空间浪费

// 构造函数
function Animal (name) { 
    this.name = name; 
} 
// 修改构造函数的原型对象为新的对象
// 增加一个constructor指向源对象,从而不破坏原型对象的结构
Animal.prototype = {
    constructor: Animal,
    eat: function(food) { 
        console.log(this.name + '正在吃:' + food); 
    },
    sleep: function(){
        console.log(this.name + '正在睡觉!'); 
    } 
}
// 创建实例
var cat = new Animal('cat');

console.log(cat.name); //cat 
console.log(cat.eat('fish')); //cat正在吃:fish
console.log(cat.sleep());//cat正在睡觉!
console.log(cat instanceof Animal); //true 

3. 原型继承

构造函数的原型是另一个构造函数的实例,原型链的实现就是基于此构想

function Parent () {
    this.name = "kevin";
}
Parent.prototype.getName = function () {
    return this.name
}
function Child () {
}
Child.prototype = new Parent();

let child1 = new Child();

console.log(child1.getName()) // kevin

4. 指定继承

利用Object.creat()创建一个继承指定构造函数的实例

适用场景

创建一个不继承任何父类构造函数的子类,如代码中临时创建一个对象

const obj = Object.creat(null);  
const obj1 = {} // 会继承Object的原型

创建一个继承指定父类构造函数的子类

const o1 = Object.creat(o); 

优点:子类__proto__指针指向的原型对象上不会携带无用的属性和方法,减少了内存的占用

5. 构造继承

利用call,apply改变构造函数内的this指向

function Animal(name,age,sex){
  this.name = name;
  this.age = age;
  this.sex = sex;
}

function Cat(name,age,sex){
    Animal.call(this,name,age,sex); // this指向Cat
    Animal.apply(this,[name,age,sex]); // this指向Cat
    this.eat = function(){}
}

优点:可以向构造函数传递参数

缺点:由于直接调用构造函数,所以构造函数必须完全适用于实例,使用场景有一定的局限性,构造函数中可能存在实例不需要的属性和方法,直接调用会造成内存空间的浪费。

6. 拷贝继承(mixin)

将构造函数的属性和方法拷贝一份到实例的原型对象中(深拷贝、浅拷贝、扩展运算符{...object}或[...array])

function Cat(name){
  var animal = new Animal();
  
  for(var p in animal){
    Cat.prototype[p] = animal[p];
  }
     
  Cat.prototype.name = name;
}

优点:支持多继承

缺点:由于存在拷贝,所以效率低,占用内存过多

7. 实例继承

  • 创建一个有共同属性和方法的构造函数
  • 调用构造函数创建一个实例
  • 向实例中添加特有的属性和方法,返回实例作为子类
function Animal(age,sex){
  this.age = age;
  this.sex = sex;
}

function Cat(name){
  var instance = new Animal();
  instance.name = name;
  return instance;
}

优点:不限制调用方式

缺点:不能实现多继承

8. 组合继承

  • 通过构造继承实现子类对父类构造函数属性的继承
  • 通过原型链继承实现子类对父类构造函数原型的继承
function Cat(name){
  Animal.call(this,name); // 属性继承
}

Cat.prototype = new Animal(); // 原型继承
Cat.prototype.constructor = Cat;

优点:可以向父类构造函数传递参数,可以获取父类构造函数原型上的属性和方法

缺点:子类的原型是父类构造函数的实例,所以子类对象的原型中多了很多不必要的属性和方法,占用内存

9. ES6的extends继承

super方法相当于调用了父类的constructor

//父类
class Person {
    //constructor是构造方法
    constructor(skin, language) {
        this.skin = skin;
        this.language = language;
    }
    say() {
        console.log('我是父类')
    }
}
//子类
class Chinese extends Person {
    constructor(skin, language, positon) {
        super(skin, language);
        this.positon = positon;
    }
    aboutMe() {
        console.log(${this.skin} ${this.language}  ${this.positon});
    }
}
//调用只能通过new的方法得到实例,再调用里面的方法
let obj = new Chinese('红色', '中文', '香港');
obj.aboutMe();
obj.say();

优点:创建子类时可以向父类构造函数传递参数,子类不再臃肿,原型只包含父类构造函数的原型

第十一章 JS单线程机制

1. 线程和进程

  • 进程:CPU资源分配的最小单位
  • 线程:CPU调度的最小单位

两者关系

  • 线程就是程序中的一个执行流,一个进程可以有一个或多个线程
  • 进程之间相互独立,但同一进程下的各个线程间有些资源是共享的

2. 线程分类

  • 单线程

一个进程中只有一个执行流叫作单线程,程序执行时,所走的程序路径按照顺序排队执行,前面的执行完毕后面的才可以执行

  • 多线程

一个进程中有多个执行流称作多线程,程序可以同时运行多个不同的线程来执行多个不同的任务

3. 线程的生命周期

1)新建状态

概念:使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态 特点:它会保持这个状态直到程序 start() 这个线程

2)就绪状态

概念:当线程对象调用了 start() 方法之后,该线程就进入就绪状态 特点:就绪状态的线程处于就绪队列中,只要获得 CPU 的使用权就可以立即运行

3)运行状态

概念:就绪状态的线程获取到CPU 资源,开始执行 run(),此时线程便处于运行状态 特点:处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态

4)阻塞状态

概念:如果一个线程执行了 sleep(睡眠)、suspend(挂起)、wait(等待)等方法,失去所占用CPU资源之后,该线程就从运行状态进入阻塞状态 特点:如果睡眠时间已到或者又获得CPU资源后可以重新进入就绪状态

等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态 同步阻塞:由于同步锁被其他线程占用,导致该线程在获取 synchronized 同步锁时失败 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态,当 sleep() 状态超时、join() 等待线程终止或超时、或者 I/O 处理完毕,线程会重新转入就绪状态

5)死亡状态

一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到死亡状态

4. WebWorker

Web Worker 是HTML5 标准的一部分,在js主线程之外开辟新的Worker线程,使js实现多线程的能力,Worker线程与js主线程能够同时运行,互不阻塞。

  • worker线程不能直接操作DOM节点,也不能使用window对象的默认方法和属性(worker运行的上下文并非主线程中的全局上下文)
  • worker线程的运行不会影响主线程,但与主线程交互时仍受到主线程单线程的瓶颈制约
  1. 适用场景:
  • 复杂计算(大量计算或复杂的数据处理,如图像处理、音频处理、视频处理等)
  • 后台下载(如从服务器获取大量数据)
  • 数据处理
  • canvas图像绘制
  • 资源懒加载
  • 资源分析
  1. worker线程分类
  • 专用worker(Dedicated Web Worker)
    • 只能被创建它的脚本使用,一个专用线程对应一个主线程
    • 浏览器支持度高
    • 专用线程与主线程通过onmessage事件和potMessage()方法完成通信
    • 专用线程与主线程通过复制传递数据,而不是在内存中共享数据
    • 专用线程与主线程通信通过序列化和反序列实现数据的复制
    • 专用线程与主线程之间适合传递基本类型,不适合传递复杂的引用类型
const worker = new Worker('worker.js');
// 接收信息
worker.onmessage = e => {}; 
// 发送信息
worker.postMessage('Greeting from Main.js'); 
  • 共享worker(Shared Web Worker)
    • 可以在不同的脚本中使用,一个共享线程对应多个主线程
    • 浏览器支持度低
    • 对应的多个主线程必须满足同源策略
    • 主线程与共享线程通过MessagePort对象建立链接
    • 共享线程与主线程通信通过序列化和反序列实现数据的复制
// 创建共享线程
const worker = new SharedWorker('./sharedWorker.js');
// 开启端口
worker.port.start(); 
// 接收数据
worker.port.addEventListener('message',msg =>{
    console.log(msg.data);
});
// 发送数据
worker.port.postMessage(data);
// 关闭线程
worker.port.close();

共享线程应用举例

shareWorker.js

const portsList = []; //所有连接这个worker的集合

self.onconnect = (event) => { // 连接成功回调
  const port = event.ports[0];
  portsList.push(port);
  port.onmessage = (event) => {
    const { message, value } = event.data;
    let result = 0;
    switch (message) {
      case 'add':
        result = value * 2
        break;
      case 'multiply':
        result = value * value
        break;
      default:
        result = value
    }
    portsList.forEach((port) => port.postMessage(`${port}端口${message}结果是:${result}`));
  }
}

page1

sharedWorkerHook.port.start();
sharedWorkerHook.port.onmessage = (event) => {
  console.log(event.data);
};
setTimeout(()=>{
  sharedWorkerHook.port.postMessage({ message: 'add', value: 1 })
},1000)

page2

sharedWorkerHook.port.start();
sharedWorkerHook.port.onmessage = (event) => {
  console.log(event.data);
};
setTimeout(()=>{
  sharedWorkerHook.port.postMessage({ message: 'multiply', value: 1 })
},3000)
区别专用线程共享线程
使用范围不同只能被创建它的脚本使用可以被同源下的多个脚本所共享
通信机制不同点对点通信广播机制通信
生命周期不同脚本结束或浏览器关闭则线程终止作用域为进程级别,只要还有一个窗口使用则线程保持运行状态
  1. 使用worker线程
const worker = new Worker(path, options);  
  • path:有效的js脚本的地址,必须遵守同源策略
  • options.type:可选,指定worker类型,取值classic 或 module,默认值classic
    • module:支持ES6的模块化语句
  • options.credentials:可选,指定 worker 凭证,取值omit、same-origin、 include,默认值 omit (不要求凭证)
  • options.name:可选,表示worker的scope的一个DOMString值,主要用于调试目的

数据传递 主线程与worker线程都是通过postMessage方法来发送消息,监听message事件来接收消息 

主线程:

// 创建worker 
const myWorker = new Worker('/worker.js'); 
// 接收信息
myWorker.onmessage = e => {}; 
// 发送信息
myWorker.postMessage('Greeting from Main.js'); 
// 当worker内部出现错误时触发
myWorker.addEventListener('error', err => { 
    console.log(err.message); 
}); 
// 当message事件接收到无法被反序列化的参数时触发
myWorker.addEventListener('messageerror', err => { 
    console.log(err.message) 
});

worker线程:

self.addEventListener('message', e => { 
    // 接收到消息 
    console.log(e.data); 
    // 向主线程发送消息  
    self.postMessage('Greeting from Worker.js');
});
// 当worker内部出现错误时触发
self.addEventListener('error', err => { 
    console.log(err.message); 
}); 
// 当message事件接收到无法被反序列化的参数时触发
self.addEventListener('messageerror', err => { 
    console.log(err.message) 
});
  • error:当worker内部出现错误时触发
  • messageerror:当message事件接收到无法被反序列化的参数时触发
  1. 引入脚本与库

Worker线程能够访问一个全局函数importScripts()来引入脚本,该函数接受0个或者多个URI作为参数来引入资源

importScripts(); // 什么都不引入
importScripts('foo.js'); // 只引入 "foo.js"
importScripts('foo.js', 'bar.js'); // 引入两个脚本
  1. 关闭worker线程 

1)主线程关闭

const myWorker = new Worker('/worker.js'); myWorker.terminate();  

2)worker线程关闭

self.close();  

worker线程当前任务队列中的任务会继续执行,下一个任务队列会被直接忽略,不会继续执行

主线程关闭:主线程与worker线程之间的连接会立刻停止

worker线程关闭:不会直接断开与主线程的连接,而是等 worker 线程当前任务队列中的所有任务执行完后再关闭,即在当前事件中继续调用postMessage() 方法,主线程还是能通过监听message事件接收消息

第十二章 执行栈和执行上下文

1. 执行栈

栈是一种先进后出的数据结构,执行栈用来存储执行上下文

2. 执行上下文

当函数在执行的前一刻,会创建一个内部对象,叫做执行上下文,执行上下文定义了一个函数执行时的环境,包括变量对象,作用域链,this指针指向

多次调用同一个函数会创建多个执行上下文;

生命周期

  • 函数执行前,创建执行上下文;
  • 函数执行完毕时,执行上下文会被销毁;

3. 执行上下文分类

  • 全局执行上下文

默认或基础的执行上下文,一个程序中只会存在一个全局上下文,它在整个 javascript 脚本的生命周期内都会存在于执行堆栈的最底部,且不会被栈弹出销毁。全局上下文会生成一个全局对象(以浏览器环境为例,这个全局对象是 window),并且将 this 值绑定到这个全局对象上

  • 函数执行上下文

每当一个函数被调用时,都会创建一个新的函数执行上下文(不管这个函数是不是被重复调用的)

  • Eval 函数执行上下文

执行在 eval 函数内部的代码也会创建执行上下文,eval() 是全局对象的一个函数属性

4. js代码执行流程

1)创建执行栈:在执行一段代码时,JS 引擎会首先创建一个执行栈,用来存放执行上下文

2)创建执行上下文:JS 引擎会创建一个全局执行上下文,并 push 到执行栈中, 在这个过程中JS 引擎会为这段代码中所有变量分配内存并赋一个初始值(undefined)

3)执行执行上下文:创建完成后,JS 引擎进入执行阶段,在这个过程中JS 引擎会逐行的执行代码,即为之前分配好内存的变量逐个赋值(真实值),如果存在 function 的调用,那么 JS 引擎会创建一个函数执行上下文,并 push 到执行栈中,其创建和执行过程跟全局执行上下文一样

4)执行上下文出栈并等待回收:当一个执行栈执行完毕后该执行上下文就会从栈中弹出,接下来会进入下一个执行上下文

全局执行上下文环境永远在栈底,而当前正在执行的函数上下文在栈顶

第十三章 事件循环机制

1. 同步任务

在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务

2. 异步任务

不进入主线程、而进入任务队列的任务(任务队列中的任务与主线程并列执行)。当主线程空闲、任务队列通知主线程某个异步任务可以执行时该任务才会进入主线程执行。

由于是队列存储所以满足先进先出规则。

常见的异步任务有setInterval、setTimeout、promise.then等

3. 事件执行机制

  • 任务分别进入不同的执行"场所":同步任务进入主线程、异步任务进入Event Table并注册函数
  • 当指定的任务完成时,Event Table 会将这个函数移入任务队列
  • 主线程内的任务全部执行完毕,会去任务队列读取对应的函数,进入主线程执行

4. 宏任务和微任务

js中异步任务被分为两种,宏任务MacroTask和微任务MicroTask

微任务:在当前任务执行完毕后立即执行的任务

宏任务:需要排队等待 JavaScript 引擎空闲时才能执行的任务

常见的宏任务

  • setTimeout(),时间 = 插入队列的时间+等待的时间
  • setInterval()
  • requestAnimationFrame()
  • postMessage()
  • MessageChannel()
  • I/O 操作
  • DOM 事件

常见的微任务

  • Promise.then()
  • Promise.catch()
  • Promise.finally()
  • MutationObsever()

5. 任务执行顺序

JS开始执行... ...

第一步:将任务分为同步任务、异步任务

第二步:同步任务进入主线程依次执行,异步任务区分宏任务和微任务并分别进入不同的Event Table

第三步:宏任务进入到 Event Table 中,会注册回调函数,每当指定的事件完成时,Event Table 会将这个回调函数移到 Event Queue 中;微任务也会进入到另一个 Event Table 中,并在里面注册回调函数,每当指定的事件完成时,Event Table 会将这个函数移到 Event Queue 中;

第四步:当主线程内的任务执行完毕,主线程为空时,会检查微任务的 Event Queue,如果有任务,就全部执行(按顺序),如果没有就执行下一个宏任务;每执行一个宏任务就等同于清空微任务队列,所以宏任务是一个个执行,微任务是全部执行

console.log('1');
 
setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})
 
setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})
// 1,7,6,8,2,4,3,5,9,11,10,12

第十四章 模块化

模块化是一种最主流的代码组织方式,是一种开发思想,一个模块就是一个实现特定功能的文件,把代码按照功能划分为不同的模块单独维护

1. 模块化特点

  • 避免全局污染、命名冲突等问题
  • 提高代码复用率
  • 能够进行依赖关系的管理

2. 模块化演变

1)文件划分方式

将每个功能和相关的状态数据单独存放在一个文件中,这个文件就是一个独立的模块,使用时将这个模块引入页面中,直接调用模块中的成员(变量/函数)

特点:一个script标签就对应一个模块,所有模块都在全局作用域内

缺点:

  • 各模块内部的成员都处在全局作用域中,即任意位置均可进行访问和修改,这样就会造成全局污染
  • 容易出现命名冲突
  • 无法很好地管理各模块之间的依赖关系
let msg = 'modulel'

function foo() {
    console.log('foo()', msg);
}

function bar() {
    console.log('bar()', msg);
}

<script type='text/javascript'>
    foo();
    bar();
    msg='NBA';//污染全局变量
    foo();
</script>

2)命名空间方式

在文件划分方式的基础上,约定每个模块只暴露一个对象,并将该模块中的所有成员封装在该对象中,当需要使用的时候,就调用这个对象的属性即可

// module_a.js
let moduleA = {
  name: '一碗周',
  handle() {
    console.log(this.name)
  },
}

// module_b.js
let moduleB = {
  name: '一碗粥',
  handle() {
    console.log(this.name)
  },
}

// html
<body>
  <script src="./component/module_a.js"></script>
  <script src="./component/module_b.js"></script>
  <script>
    console.log(moduleA.name);
    console.log(moduleB.name);//仍然可以访问到模块中的所有属性
    moduleA.handle()
    moduleB.handle()
  </script>
</body>

特点:实际上就是简单的对象封装

优点:减少了命名冲突的可能

缺点:各模块中仍然没有私有空间,无法很好地管理各模块之间的依赖关系

3)IIFE(立即执行函数)

使用立即执行函数去创建闭包,这种方式为模块提供了私有空间

将模块中每一个成员都放在一个函数提供的私有作用域当中,对于需要暴露给外部的成员可以通过挂载到全局对象上的方式去实现。

// module_a.js
(function () {
  let name = '一碗周'
  function handle() {
    console.log(name)
  }
  window.moduleA = { handle }//向window暴露handle对象,从而形成闭包
})()

// module_b.js
(function () {
  let name = '一碗粥'
  function handle() {
    console.log(name)
  }
  window.moduleB = { handle }
})()

// html
<body>
  <script src="./component/module_a.js"></script>
  <script src="./component/module_b.js"></script>
  <script>
    moduleA.handle()
    moduleB.handle()
  </script>
</body>

优点:实现了各模块私有空间

缺点:无法很好地管理各模块之间的依赖关系

4)IIFE模式增强

在 IIFE 模式的基础上,通过为立即执行函数添加参数的形式,实现模块间的依赖

// module_a.js
(function () {
  function printName(name) {
    console.log(name)
  }
  // 暴露一个打印的方法
  window.moduleA = { printName }
})()

// module_b.js
(function (m){
  let name = '一碗周'
  function sayName() {
    // 使用其他模块的成员
    m.printName(name)
  }
  window.moduleB = { sayName }
})(moduleA)

// html文件
<body>
  <script src="./component/module_a.js"></script>
  <script src="./component/module_b.js"></script>
  <script>
    moduleB.sayName()
  </script>
</body>

缺点:引入了过多script标签,就需要发送多个请求,请求数量太多,依赖关系模糊且难以维护

3. CommonJS

CommonJS规范指出一个单独的文件就是一个模块,它采用的是同步加载模块,也就是说模块加载的顺序和代码中编写的顺序是一致的,而加载的文件资源大多数都存储在服务器中,所以说加载速度没有什么问题。 但是这种方案不适用于浏览器端,浏览器采用的是异步加载(CMD、AMD和ESmodule)

  • 通过module+exports导出
module.exports = {
  getName() {
    return name
  },
  setName(n) {
    name = n
  }
}
  • 通过require导入
const person = require('./module_c')

CommonJS的模块加载机制是被输出值的拷贝,导入值与输出值不存在关联

  • 从框架层面解决模块依赖关系的管理及全局变量污染的问题
  • 不适用于客户端
  • 无法解决异步依赖问题

4. 前端模块化

  • AMD

通过异步加载及允许制定回调函数来实现模块化管理,成熟的第三方库有require.js

  • CMD

阿里编写的sea.js就是CMD,类似于CommonJS在前端的实现,解决了前端的模块化问题,去年已停止更新

  • AMD与CMD区别
AMDCMD
依赖前置、提前执行依赖就近、延迟执行