前端进阶必经之路(一):1.2w字深入理解JavaScript26个核心概念

36,414 阅读35分钟

简介

大家好,我是六六。花费了一周的时间终于写完了这篇js核心概念的汇总。创作的过程也是查缺补漏,希望大家也能有收获的。喜欢的给作者点个赞哦,创作不容易的。

系列预告

2.前端进阶必经之路(二):前端必会的20个手写及算法 3.前端进阶必经之路(三):前端必会的Vue的7大手写源码

备注

  • 如有错误,请大家积极指出来,避免误导他人。(第一次写这么多字的文章,难免会有错误的)
  • 如有补充,告诉我,我会积极添加上来。
  • 我也是个小白,希望和大家一起学习进步。所以喷我的话,我也会喷回去的哦。
  • 如果不慎引入他人的文章片段,指出来我会添加原处的。

系列

目录:

1.js运行环境 2.js数据类型 3.js类型转换 4.js遍历 5.作用域与作用域链 6.闭包 7.原型及原型对象 8.this指向问题 9.继承 10.js数据存储与传参 11.深拷贝与浅拷贝(手写一个深拷贝) 12.高阶函数及柯里化 13.ajax 14.DOM 15.BOM 16.包装对象 17.封装多态 18.异常捕获try catch 19.单线程和事件循环 20.promise 21.es6模块化

1.js运行环境

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

渲染引擎:负责网页内容呈现的。

Js引擎:解释js脚本,实现js交互效果的。

1.1常见的内核:

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

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

async:开启另外一个线程下载js文件,下载完成,立马执行。(此时才发生阻塞)

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

2.js数据类型

基本数据类型:

string:由多个16位Unicode字符组成的字符序列,有单引号或双引号表示

number:采用了IEEE754格式来表示整数和浮点数值

boolean:有两个字面值,true和false.区分大小写的

null:只有一个值的数据类型,值为null.表示一个空对象指针,但用typeof操作会返回一个对象。一般 我们把将来用于保存对象的变量初始化为null.

undefined:这个类型只有一个值,在声明变量未进行赋值时,这个变量的值就是undefined.

Symbol:唯一的值。

引用数据类型:

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

经典面试题:

0.1+0.2为什么不等于0.3?

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

1.typeof

typeof检测null是一个对象

typeof检测函数返回时一个function

typeof检测其他对象都返回 object

2.instanceof(下一节手写)

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

3. constructor:

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

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

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

3.js类型转换

javaScript作为一门弱类型语言,本质为一个变量可以被赋予不同的数据类型。代码简洁灵活,但稍有 不慎,会出现很多坑。

javaScript也作为一门动态类型语言,在运行时,可以随便改变其变量的结构。

所以js变量可以做任意的类型转换,有两种方式,显示类型转换和隐士类型转换。

但是能转换的类型只有三种:to Number,to String,to Boolean.

当基本类型转换成上述类型时会调用:Number() ,String(), Boolean()

只有'' 0 null undefined NaN false转换boolean为false,其他都为true

当引用类型转换时,就稍微有些复杂,我们来举个例子:(所有对象转换boolean都为true)

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))   // 你好啊

当对象进行类型转换时:

1.首先调用valueOf,如果执行结果是原始值,返回,如果不是下一步

2.其次调用toString,如果执行结果是原始值,返回,如果不是,报错。

特殊情况:

当使用显示类型转换成String时,执行顺序则是先调用toString,其次调用valueOf

显示类型转换:

Number() / parseFloat() / parseInt()/String() / toString()/Boolean()

隐士类型转换:

+ - == !><= <= >=

3.1经典面试题:(都能答对真的很厉害了,留个名让我关注膜拜一下)

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采用的静态作用域,也可以称为词法作用域,意思是说作用域是在定义的时候就创建了, 而不是运行的时候。此话对于初学者很不好理解,看看下面这个例子:

let a=1
function aa(){
	console.log(a)    //输出1
}
function bb(){
	let a=2
    aa()
}

是不是非常违背常理啊,你看嘛,aa在bb里面调用的,aa函数里面没有a变量,那么就应该去调用它的作 用域里找,刚好找到a等于2。思路是完美的,可是js的作者采用的静态作用域,不管你们怎么运行,你们 定义的时候作用域已经生成了。

那么什么是作用域?

变量和函数能被有效访问的区域或者集合。作用域决定了代码块之间的资源可访问性。

作用域也就是一个独立的空间,用于保护变量防止泄露,也起到隔离作用。每个作用域里的变量可以相同命名,互不干涉。就像一栋房子一样,每家每户都是独立的,就是作用域。

作用域又分为全局作用域和函数作用域,块级作用域。 全局作用域任何地方都可以访问到,如window,Math等全局对象。 函数作用域就是函数内部的变量和方法,函数外部是无法访问到的。 块级作用域指变量声明的代码段外是不可访问的,如let,const.

5.2作用域链

知道作用域后,我们来说说什么是作用域链?

表示一个作用域可以访问到变量的一个集合。函数作为一个对象有一个[[scope]]属性,就是表示这个集合的。再来理解几个概念词:

AO:活动变量(Active object,VO)

VO:变量对象(Variable object,VO)

执行上下文:代码运行的环境,分为全局上下文和函数上下文。

举例子来说明一下:(借用的例子)
// 作者:jianyangdu洋仔
function a() {
        function b() {
            var b = 234;
        }
        var a = 123;
        b();
    }
    var gloab = 100;
    a();

第一步: a 函数定义

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

第二步: 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。

第三步: 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。

第四步: 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。 以上就是上面代码执行完之后的结果。

6.闭包

引自:https://github.com/mqyqingfeng/Blog/issues/9

不会闭包的程序员不是好程序员。

闭包的官方定义:

mdn:闭包是指那些能够访问自由变量的函数。

维基百科:在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。闭包可以用来在一个函数与一组“私有”变量之间创建关联关系。在给定函数被多次调用的过程中,这些私有变量能够保持其持久性.

我:一个作用域可以访问另一个作用域的变量,就产生闭包。之前比喻作用域就好比一栋房子每一户,闭包相当于串门。

什么是自由变量?

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

闭包=函数+函数能够访问的自由变量。

从技术的角度讲,所有的JavaScript函数都是闭包。

var a = 1;

function foo() {
    console.log(a);
}

foo()

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

那么,函数 foo + foo 函数访问的自由变量 a 不就是构成了一个闭包嘛……

我们在看看ECMAScript中,闭包指的是:

1.从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。

2.从实践角度:以下函数才算是闭包:

  • 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
  • 在代码中引用了自由变量

下面我们开始实践论证:(例子依然是来自《JavaScript权威指南》)

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
var foo = checkscope();
foo();
输出 "local scope"

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

[
	AO:{
    	scope:"local scope",
        f:function
    },
    global:{
     scope :"local scope",
     checkscope:function
	}
]

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

[
	AO:{
    	arguments:[],
        this:window
    },
	AO:{
    	scope:"local scope",
        f:function
    },
    global:{
     scope :"local scope",
     checkscope:function
	}
]
根据先后顺序scope变量输出为"local scope"

经典面试题:

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 为:

globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}

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

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

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

7.原型及原型对象

不多说,要会背下面这张图,理解起来很简单的。

先谈一下我的理解吧:

javascript万物皆对象,每个对象都有一个__proto__属性,指向了创造它的构造函数的原型对象。

每个函数都有一个原型对象,prototype,当使用new 创造对象时继承这个对象。

function A(){}
var a=new A()
a.__proto__===A.prototype

下面就有问题了,谁创造了A这个构造函数呢,还有谁创造了A.prototype这个对象呢?

这时候我们就要知道js两个顶级函数,Function,Object

所有函数都是由Function创建的

A.__proto__===Function.prototype

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

Function.__proto__===Function.prototype

Object刚讲的是顶级函数,所以也是函数:(所有的鱼都归猫管哈哈哈哈哈)

Object.__proto__===Function.prototype

所有的对象都是由Object构造函数创建的:

A.prototype.__proto__===Object.prototype

那么Object.prototype也是对象啊,是由谁创建的呢,记住万物皆空,何尝不是人生,到头来什么都会没有。

Object.prototype.__proto__===null

原型链(一种访问机制):

1.在访问对象的某个成员的时候会先在对象中找是否存在

2.如果当前对象中没有就在构造函数的原型对象中找

3.如果原型对象中没有找到就到原型对象的原型上找

4.直到Object的原型对象的原型是null为止

8.this指向问题

这个问题直接从结果入手,this指向一共有七种情况,下面一 一说起。

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

const obj={a:this}
obj.this===window  //true
function fn(){
    console.log(this)   //window
}

8.2 构造函数

  function a() {
    console.log(this)
  }
  const obj = new a()   //  a{}
  a()                    // 'window'

new出来的对象,this指向了即将new出来的对象。
当做普通函数执行,this指向window。

8.3 对象方法

  const obj = {
    x: 0,
    foo: function () {
      console.log(this)
    }
  }
  obj.foo()                 // obj
  const a = obj.foo
  a()                       //window

作为对象方法,this指向了这个对象。(新对象绑定到函数调用的this)
一旦有变量直接指向了这个方法,this为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属性

  function Fn() {
    this.a = 10
    let a = 100
  }
  Fn.prototype.fn = function () {
    console.log(this.a)             // 10 说明指向了obj这个对象
  }
  const obj = new Fn()
  obj.fn()

原型定义方法的this指向了实例对象。毕竟是通过对象调用的。

8.5 call ,apply, bind

  const obj = {
    x: 10
  }
  function fn() {
    console.log(this)
  }
  fn.call(obj)      //obj
  fn.apply(obj) //obj
  fn.bind(obj)() //obj

this指向传入的对象。

8.6 DOM事件

 document.getElementById('app').addEventListener('click', function () {
    console.log(this)           // id为app的这个对象
  })

指向绑定事件的对象。

8.7 箭头函数

  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.继承

  • 原型链继承
  • 构造函数继承
  • 组合继承
  • 寄生组合继承
  • extends继承

9.1 原型链继承:

 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 构造函数继承:

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 组合继承:

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

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原型式继承

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

  • 不能做到函数复用
  • 共享引用类型属性的值
  • 无法传递参数 缺点:
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 寄生式继承

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

缺点同上

 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)

借用阮一峰老师的es6中extends继承,眼过千遍,不如手写一遍。

上面的只能说去应付面试,这个才是我们开发中最常用的,所以必须掌握。

写法:
class Point {
}

class ColorPoint extends Point {
}

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

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

Super关键字

class A {}

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

上面代码中,子类B的构造函数之中的super(),代表调用父类的构造函数。这是必须的,否则 JavaScript 引擎会报错。

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

class A {
  constructor() {
    console.log(new.target.name);
  }
}
class B extends A {
  constructor() {
    super();
  }
}
new A() // A
new B() // B

上面代码中,new.target指向当前正在执行的函数。可以看到,在super()执行时,它指向的是子类B的构造函数,而不是父类A的构造函数。也就是说,super()内部的this指向的是B。

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

class A {}

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

上面代码中,super()用在B类的m方法之中,就会造成语法错误。

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

class A {
  p() {
    return 2;
  }
}

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

let b = new B();

上面代码中,子类B当中的super.p(),就是将super当作一个对象使用。这时,super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()。

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

class A {
  constructor() {
    this.p = 2;
  }
}

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

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

上面代码中,p是父类A实例的属性,super.p就引用不到它。

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

class A {}
A.prototype.x = 2;

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

let b = new B();

上面代码中,属性x是定义在A.prototype上面的,所以super.x可以取到它的值。

ES6 规定,在子类普通方法中通过super调用父类的方法时,方法内部的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

上面代码中,super.print()虽然调用的是A.prototype.print(),但是A.prototype.print()内部的this指向子类B的实例,导致输出的是2,而不是1。也就是说,实际上执行的是super.print.call(this)。

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

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.x赋值为3,这时等同于对this.x赋值为3。而当读取super.x的时候,读的是A.prototype.x,所以返回undefined。

如果super作为对象,用在静态方法之中,这时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在静态方法之中指向父类,在普通方法之中指向父类的原型对象。

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

10.数据存储与传参

在JavaScript中,每一个变量在内存中都需要一个空间来存储。内存空间又分为栈内存和堆内存。 基本数据类型保存在栈中,引用类型保存于堆中。
let a='a'
a='b'
let b=a
  • 首先创建变量a,在创建字符串'a',使变量a指向'a',a保存的是这个值
  • 接着又创建字符串'b',使变量a指向字符串'b',同时删除'a'
  • 深拷贝了一份a(重新创建了'b'),使变量b指向了'b',变量a和变量b互不受影响。
let obj1=new Object()
let obj2=obj1
obj1.name='node'
alert(obj2.name)           //node
  • 创建一个对象,开辟了一个堆内存,使变量obj1指向这个对象(堆内存)的地址
  • 创建一个变量obj2,将obj1的值赋值给obj2,就是将地址赋值给了obj2,这时obj1和obj2指向的是同一个堆内存
  • 两个变量就相互影响。

传递参数

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

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.深拷贝与浅拷贝(手写一个深拷贝)

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

浅拷贝:

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

深拷贝:

1.JSON

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)

输出结果

{
  arr: [ 1, 1 ],
  obj: { key: 11 },
  date: '2020-04-15T07:13:36.087Z',
  reg: {},
  null: null
}
false
false

总结:输出结果发现,函数和undefine都不见了,正则变成一个空对象了。

2.递归+for in

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

总结:无法拷贝函数,正则,日期对象。

环就是对象循环引用自己。比如:

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

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

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.

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.高阶函数及柯里化

首先看一道面试题?

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

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

接受参数为函数

所以我们来实现一下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]

返回函数

柯里化

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

再来看看之前的面试题:
如何实现add(1)(2)(3)结果等于6?
如果参数是100个呢,难道要写100次吗,如果参数是未知的呢,所以这个写法没有通用性。
例如:
add(1)(2)(3)                
add(1, 2, 3)(4)           
add(1)(2)(3)(4)(5)          
add(2, 6)(1)

分析:首先我们做的要把所有参数收集起来,然后进行统一处理。

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是一系列技术的统称,实现向服务器发送数据接受数据而页面不刷新。

const xhr = new XMLHttpRequest()

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不同。

取消异步

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

progress进度:

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

14.DOM

dom元素的增删改查

增:添加dom元素

  • appendChild:向当前节点的子节点列表的末尾添加新的子节点
  • insertBefore(newchild,refchild):其中参数newchiild表示插入新的节点,refchild表示在此节点前插入新的子节点,返回新的子节点:
document.body.insertAdjacentHTML(
  'beforeend',
  '<a>你好</a>'
)

这个方法允许你将任何有效的 HTML 字符串插入到一个 DOM 元素的四个位置,这四个位置由方法的第一个参数指定,分别是:

  • 'beforebegin': 元素之前
  • 'afterbegin': 元素内,位于现存的第一个子元素之前
  • 'beforeend': 元素内,位于现存的最后一个子元素之后
  • 'afterend': 元素之后
<!-- beforebegin -->
<div>
  <!-- afterbegin -->
  <span></span>
  <!-- beforeend -->
</div>
<!-- afterend --

删除dom元素

方法1const target = document.querySelector('.target')
target.parentNode.removeChild(target)
方法2const target = document.querySelector('#target')
target.remove()

移动dom:

<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>

替换dom元素

parentNode.replaceChild(newNode, oldNode)

oldElement.replaceWith(newElement)

dom检测:

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

dom观察者:MutationObserver

在处理用户交互的时候,当前页面的 DOM 元素通常会发生很多变化,而有些场景需要开发者们监听这些变化并在触发后执行相应的操作。MutationObserver 是浏览器提供的一个专门用来监听 DOM 变化的接口,它强大到几乎可以观测到一个元素的所有变化,可观测的对象包括:文本的改变、子节点的添加和移除和任何元素属性的变化。 如同往常一样,如果想构造任何一个对象,那就 new 它的构造函数:

const observer = new MutationObserver(callback)

复制代码传入构造函数的是一个回调函数,它会在被监听的 DOM 元素发生改变时执行,它的两个参数分别是:包含本次所有变更的列表 MutationRecords 和 observer 本身。其中,MutationRecords 的每一条都是一个变更记录,它是一个普通的对象,包含如下常用属性:

  • type: 变更的类型,attributes / characterData / childList
  • target: 发生变更的 DOM 元素
  • addedNodes: 新增子元素组成的 NodeList
  • removedNodes: 已移除子元素组成的的 NodeList
  • attributeName: 值发生改变的属性名,如果不是属性变更,则返回 null
  • previousSibling: 被添加或移除的子元素之前的兄弟节点
  • nextSibling: 被添加或移除的子元素之后的兄弟节点

根据目前的信息,可以写一个 callback 函数了:

const callback = (mutationRecords, observer) => {
  mutationRecords.forEach({
    type,
    target,
    attributeName,
    oldValue,
    addedNodes,
    removedNodes,
  } => {
   switch(type) {
      case 'attributes':
        console.log(`attribute ${attributeName} changed`)
        console.log(`previous value: ${oldValue}`)
        console.log(`current value: ${target.getAttribite(attributeName)}`)
        break
      case 'childList':
      	console.log('child nodes changed')
        console.log('added: ${addedNodes}')
        console.log('removed: ${removedNodes}')
        break
      // ...
    }
  })
}

复制代码至此,我们有了一个 DOM 观察者 observer,也有了一个完整可用的 DOM 变化后的回调函数 callback,就差一个需要被观测的 DOM 元素了:

const target = document.querySelector('#target')
observer.observe(target, {
  attributes: true,
  attributeFilter: ['class'],
  attributesOldValue: true,
  childList: true,
})

复制代码在上面的代码中,我们通过调用观察者对象的 observe 方法,对 id 为 target 的 DOM 元素进行了观测(第一个参数就是需要观测的目标元素),而第二个元素,我们传入了一个配置对象:开启对属性的观测 / 只观测 class 属性 / 属性变化时传递属性旧值 / 开启对子元素列表的观测。 配置对象支持如下字段:

  • attributes: Boolean,是否监听元素属性的变化
  • attributeFilter: String[],需要监听的特定属性名称组成的数组
  • attributeOldValue: Boolean,当监听元素的属性发生变化时,是否记录并传递属性的上一个值
  • characterData: Boolean,是否监听目标元素或子元素树中节点所包含的字符数据的变化
  • characterDataOldValue: Boolean,字符数据发生变化时,是否记录并传递其上一个值
  • childList: Boolean,是否监听目标元素添加或删除子元素
  • subtree: Boolean,是否扩展监视范围到目标元素下的整个子树的所有元素

当不再监听目标元素的变化时,调用 observer 的 disconnect 方法即可,如果需要的话,可以先调用 observer 的 takeRecords 方法从 observer 的通知队列中删除所有待处理的通知,并将它们返回到一个由 MutationRecord 对象组成的数组当中:

const mutationRecords = observer.takeRecords()
callback(mutationRecords)
observer.disconnect()

15.BOM

BOM为浏览器对象模型,接下来一一来回顾。

Window

所有 JavaScript 全局对象、函数以及变量均自动成为 window 对象的成员。

全局变量是 window 对象的属性。

全局函数是 window 对象的方法。

甚至 HTML DOM 的 document 也是 window 对象的属性之一:

  • window.innerHeight - 浏览器窗口的内部高度(包括滚动条)
  • window.innerWidth - 浏览器窗口的内部宽度(包括滚动条)
  • window.open() - 打开新窗口
  • window.close() - 关闭当前窗口
  • window.moveTo() - 移动当前窗口
  • window.resizeTo() - 调整当前窗口的尺寸

全局变量不能通过 delete 操作符删除;而 window 属性上定义的变量可以通过 delete 删除

var num=123;
window.str="string";
delete num;
delete str;
console.log(num); //123

console.log(str); //str is not defined

访问未声明的变量会抛出错误,但是通过查询 window 对象,可以知道某个可能未声明的变量是否存在。

var newValue=oldValue; // 报错:oldValue is not defined
var newValue=window.oldValue; // 不会报错
console.log(newValue); // undefined

screen

  • screen.availWidth - 可用的屏幕宽度
  • screen.availHeight - 可用的屏幕高度

location

window.location 对象用于获得当前页面的地址 (URL),并把浏览器重定向到新的页面。

  • location.href 返回当前页面的url
  • location.hostname 返回 web 主机的域名
  • location.pathname 返回当前页面的路径和文件名
  • location.port 返回 web 主机的端口 (80 或 443)
  • location.protocol 返回所使用的 web 协议(http: 或 https:)

window.location.assign(url) : 加载 URL 指定的新的 HTML 文档。 就相当于一个链接,跳转到指定的url,当前页面会转为新页面内容,可以点击后退返回上一个页面。

window.location.replace(url) : 通过加载 URL 指定的文档来替换当前文档 ,这个方法是替换当前窗口页面,前后两个页面共用一个窗口,所以是没有后退返回上一页的

history

window.history 对象包含浏览器的历史

  • history.back() - 与在浏览器点击后退按钮相同
  • history.forward() - 与在浏览器中点击向前按钮相同
  • history.go(index):index表示前进后退的步数,0表示刷新页面

navigator

window.navigator 对象包含有关访问者浏览器的信息

16.包装对象

JS中有三个基本数据类型是比较特殊的存在,分别是String、number、Boolear,这个三个基本是由自己对应的包装对象。并且随时等候召唤。

包装对象:

其实就是对象,在对基本类型数据进行操作时,首先会创建一个相应的对象代替,操作完之后并删除。

区别:

引用类型和基本包装对象的区别在于,生存期,引用类型所创建的对象,在执行期间一直在内存中,而基本包装对象只是存在一瞬间(也就是执行完后变量就变成null)。

var a='aa'
typeof a // 'string'
a.slice(1)  //'a'

a是一个基本类型的数据,是不具备任何方法的,但是我们可以调用slice方法
这个时候就是被包装了一层对象。
居然是对象的话,我们可以添加属性

a.name='aaa'
console.log(a.name)   //undefined

但是访问却访问不到,就像之前说的,包装对象在进行操作完就删除了,也就是说,a.name只存在了一瞬间。

17.封装,多态

多态:

多态就是通过对传递的参数判断来执行逻辑,即可实现一种多态处理机制。

function args(...args){
if(Array.from(args).length==1){
	console.log('一个参数')
	}
if(Array.from(args).length==2){
	console.log('二个参数')
	}
}

封装

封装就是把客观事物封装成抽象的类,隐藏属性和方法的实现细节,仅对外公开接口。也就是说,封装就是将属性和方法组成一个类的过程就称之为封装。

子面量:
var a={
	name:'nihao',
    do:function(){}
}
  • 优点:代码简单易懂
  • 缺点:创建多个对象会产生大量的代码,编写麻烦,且并没有实例与原型的概念
工厂模式:
function createPerson(name, age) {
  var person = new Object();
  person.name = name;
  person.age = age;

  person.do= function() {
    alert('hello word!');
  };
  return person;
}
const person1=createPerson('curry',31)
  • 优点:避免创建大量对象时代码的臃肿
  • 缺点: p1与p2之间没有内在联系
构造函数:
function Person(name, age) {
    // 通过this来添加属性方法
    this.name = name 
    this.age = age

    this.action = function () {
        alert('hello word')
    }
}
// 实例化对象
var p1 = new Person('尤雨溪', 18)
var p2 = new Person('阮一峰', 20)
  • 优点:实例化对象和构造函数之间存在关联
  • 缺点:浪费内存,构造函数中定义的方法名action一样,但实例化出来的对象名不一样,造成一个内存的浪费
原型模式:
function Person(name, age) {
    // 通过this来添加属性方法
    this.name = name 
    this.age = age
}

// 将共同拥有的方法挂在到原型上
Person.prototype.action = function () {
        alert('hello wodr')
    }
// 实例化对象
var p1 = new Person('尤雨溪', 18)
var p2 = new Person('阮一峰', 20)

p1.action()
p2.action()

18.异常捕获try catch

面试题:js异常能否被try catch扑捉到?

正确答案:只有在线程执行已经进入try catch未执行完的时候抛出来的的异常可以捕捉的到。 也就是说只能捕获同步js代码的错误。

示例1try{
    c.
}catch(e){
    console.log("error",e);
}
// output
Uncaught SyntaxError: Unexpected token '}'

代码报错的时候,线程执行未进入 try catch,那么无法捕捉异常。

比如语法异常(syntaxError),因为语法异常是在语法检查阶段就报错了,线程执行尚未进入 try catch 代码块,自然就无法捕获到异常。

示例2try{
	setTimeout(a,100)
}catch(e){
	 console.log("error",e);
}

Uncaught ReferenceError: a is not defined

setTimeout 里面报错,实际上是 100ms 之后执行的代码报错,此时代码块 try catch 已经执行完成,111 都已经被执行了,故无法捕捉异常。

可以捕获到:
try{
	 function d(){a.b;}
   d();
}catch(e){
	 console.log("error",e);
}
// error ReferenceError: a is not defined

promise的异常直接用catch捕获就好,不用在使用try catch包裹。

19.单线程和事件循环

单线程

js是运行在浏览器里面的,而浏览器是多进程多线程的。但是分配给Js,只有一个线程。因为Js可以操作dom,所以js和dom渲染一定是互斥。不然又是dom渲染,又是js操作dom,那么浏览器到底听谁的呢。多线程也会带来相应的复杂程度。

单线程特点是:任务依次执行,后面的必须等前面的任务完成后,才能执行下一个任务。
假如一个任务需要花费大量时间,那后面的任务都要等待,那这语言也太不行了。所以有了异步任务和回调函数。意思就是说,这个耗时的任务交给其他线程来执行,当任务执行完后,触发回调函数即可。所以就不阻碍js同步任务的执行。
现在又要一个问题,这些回掉函数是如何执行的,执行顺序又是如何?

事件循环

浏览器维护了一个事件队列,当主线程任务执行完成后,每隔相应的时间就会来事件队列查看,看看有咩有需要执行的任务。任务又分为宏任务和微任务,宏任务快于微任务。

  • 常见的宏任务:script(整体代码)、setTimeout、setInterval、I/O、setImmedidate,ajax
  • 常见的微任务:process.nextTick、MutationObserver、Promise.then catch finally 整体的执行顺序如上图所示:
  • 先执行一个宏任务
  • 检查是否有微任务,全部执行。(微任务产生的微任务也会全部执行),然后清空微任务队列
  • 渲染 -开启下一个宏任务 说的多不如做的多:
1// 作者 :凉玉
setTimeout(()=>{
    console.log('timer1')

    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)
// 简称set2
setTimeout(()=>{
    console.log('timer2')

    Promise.resolve().then(function() {
        console.log('promise2')
    })
    // 简称set3
    setTimeout(() => {
    	console.log('timer3')
    }, 0)
}, 0)

Promise.resolve().then(function() {
    console.log('promise3')
})

console.log('start')

运行的过程

循环一

  • 1、将脚本任务放入到task队列。
  • 2、从task中取出一个任务运行,运行的结果是将set1和set2放入到task中,将promise.then放入到microtask中,输出start。
  • 3、检查microtask checkpoint,看microtask队列中是否有任务。
  • 4、运行microtask中所有的任务,输出promise3。
  • 5、清空microtask队列之后,进入下一个循环。

循环二

  • 1、从task中在取出一个set1任务,运行的结果是输出timer1,将promise.then放入到microtask队列中。
  • 2、检查microtask checkpoint,看microtask队列中是否有任务。
  • 3、运行microtask中所有的任务,输出promise1。
  • 4、清空microtask队列之后,进入下一个循环。

循环三

  • 1、从task中在取出一个set2任务,运行的结果是输出timer2,将promise.then放入到microtask队列中,将set3放入到task队列中。
  • 2、检查microtask checkpoint,看microtask队列中是否有任务。
  • 3、运行microtask中所有的任务,输出promise2。
  • 4、清空microtask队列之后,进入下一个循环。

循环四

  • 1、从task中在取出一个set3任务,运行的结果是输出timer3
  • 2、检查microtask checkpoint,看microtask队列中没有任务,进入下一个循环。

循环五

  • 检测task队列和microtask队列都为空,WorkerGlobalScope对象中closing标志位为true,销毁event loop。

输出的结果

start
promise3
timer1
promise1
timer2
promise2
timer3

20.promise

阮一峰老师精讲promise

21.es6模块化

阮一峰老师精讲es6模块化

总结:

希望我的文章对大家有帮助哦,剩下两篇文章一周之内也都会发布的,基本也完成的差不多了,希望大家多多支持。