# JS 重要知识点


# 1、 Javascript 基本数据类型、如何存储的

原始类型包括 6 个: String、Number、Boolean、null、undefined、Symbol

引用类型统称 Object 类型,细分以下 5 个:Object、Array、Date、RegExp、Function

1、数据的存储形式

栈(Stack)和堆(Heap),两种基本的数据结构,栈是在内存中自动分配内存空间的,堆在内存中动态分配内存空间,不一定会释放,一般我们在项目中将对象类型手动置为 null,减少无用内存消耗。

注意: 原始类型按值形式存放栈中,可以直接按值访问

引用类型存放堆中,在栈中保存的是对象在堆中的引用地址

所以会出现下面的情况

var a = 10;
var b = a;
b = 30;
a; // 10
b; // 30

var obj1 = { name: "小明" };
var obj2 = obj1;
obj2.name = "小鹿";
obj1; // {name:'小鹿'}
obj2; // {name:'小鹿'}

2、Null

不同的对象在底层原理的存储是用二进制表示的,在 javaScript 中,如果二进制的前三位都为 0 的话,系统会判定为是 Object 类型。 null 的存储二进制是 000 ,也是前三位,所以系统判定 null 为 Object 类型。

3、数据类型的判断

typeof 与 instanceof 区别?

typeof是一元运算符,返回一个字符串,一般用来判断一个变量是否为空或者什么类型,除了 null 以及 Object 类型不能准确判断外,其他数据类型都可以返回正确类型。可以用 instanceof 来进行判断某个对象是不是另一个对象的实例。返回一个布尔类型。

instanceof 运算符用来测试一对象在其原型链中是否存在一个构造函数的 prototype 属性。

class A {}
console.log(A instanceof Function); // true

# 2、 this 指针,this 指向问题

什么是 this 指针?各种情况下的 this 指向问题?

this 就是一个对象,不同情况下 this 指向不同,以下是几种情况

  • 1、对象调用,this 指向该对象,(谁调用指向谁)

    var obj = {
      name: "小鹿",
      age: "21",
      run: function() {
        console.log(this);
        console.log(this.name + ":" + this.age);
      }
    };
    // 通过对象的方式调用函数
    obj.run(); // this 指向 obj
    
  • 2、直接调用函数,this 指向的是全局 window 对象。
function print() {
  console.log(this);
}
// 全局调用函数
print(); // this 指向 window
  • 3、通过 new 的防护,this 永远指向新创建的对象。
function Person(name, age) {
  this.name = name;
  this.age = age;
  console.log(this);
}
var xiaolu = new Person("小鹿", 22); // this => xiaolu
  • 4、箭头函数中的 this

由于箭头函数没有单独的 this 值,箭头函数的 this 与声明所在的上下文相同,也就是说调用箭头函数的时候,不会隐式的调用 this 参数,而是从定义时的函数继承上下文

const obj = {
  a : ()=>{
    console.log(this)
  }
}
this => window

改变this指向的三种方式,区别? // call 、 apply 、bind

var obj = {
  name: "xiaolu",
  age: "22"
};

function print() {
  console.log(this);
  console.log(arguments);
}
// 通过call改变this指向
print.call(obj, 1, 2, 3);
// 通过apply改变this指向
print.apply(obj, [1, 2, 3]);
// 通过bind改变this指向
let fn = print.bind(obj, 1, 2, 3);
fn();

相同点:都可以改变 this 指向,第一个参数都是 this 指向的对象,后续传参的形式

不同点:call 传参时单个传递的(数组也是可以的),apply 参数必须传数组,否则报错

call、apply 函数的执行是直接执行的,而 bind 函数返回一个函数,调用时执行。

箭头函数没有自己的 this 指针,通过 call、apply 方法调用只能传参,不能绑定 this,第一个参数会忽略

# 3、 创建对象的方式,区别

new 内部发生什么?手写实现一个 new 操作符?

通过 new 创建对象过程包括以下四个阶段:

  • 创建一个对象
  • 新对象的proto属性指向原函数的 prototype 属性(即继承原函数的原型)
  • 将这个新对象绑定到此函数的 this 上。
  • 返回新对象。
// new 生成对象过程
function creact(Fn, args) {
  let obj = {};
  obj.__proto__ = Fn.prototype; // 继承原型上的方法
  let res = Fn.apply(obj, args); // 调用函数将属性添加到obj上面
  return res instanceof Object ? res : obj; // 返回对象用返回的,否    则用obj
}
// 构造函数
function Test(name, age) {
  this.name = name;
  this.age = age;
}
Test.prototype.sayName = function() {
  console.log(this.name);
};
// 实现new 操作符
const newTest = create(Test, "小鹿", "23");
newTest.sayName(); // 小鹿

字面量、new 构造函数 和 Object.creact() 三种创建对象的不同

  • 字面量创建对象代码更少,易读
  • 字面量创建对象比 new 一个对象更快,因为 new 时会顺着作用域链向上查找,直到找到 Object()函数为止
  • Object.create()方式创建对象,一般用来继承
Object.create(proto, [propertiesObject]);
var People = function (name){ this.name = name; };
People.prototype.sayName = function(){console.log(this.name)};
fucntion Person(name,age){
  this.age= age;
  People.call(this,name); // 使用call继承People属性
}
// 使用Object.create()方法,实现People原型方法的继承,并修改contructor指向
Person.prototype = Object.create(Prople.prototype,{
  constructor:{
    configurable:true,
    enumerable:true,
    value:Person,
    writable:true
  }
});
var p1 = new Person('person1', 25);
p1.sayName();  //'person1'

# 4、 闭包

什么是作用域?什么是作用域链?

function fn1() {
  let a = 1;
}

function fn2() {
  let b = 2;
}

声明两个函数,分别创建量两个私有的作用域,一个函数就是一个作用域。每个函数都有一个作用域,查找变量或函数时,由局部作用域到全局作用域依次查找,这些作用域的集合就称为作用域链

什么是闭包?

函数执行,形成一个私有的作用域,保护里面私有变量不受外界干扰,除了保护私有变量外,还可以保护一些内容,这样的模式叫闭包。

闭包的作用有两个,保护和保存。

保护的应用

  • 团队开发,每个开发将自己的代码放在一个私有的作用域中,防止相会之间的变量名冲突,通过 return 或 window.xxx 的方式暴露全局下
  • jQuery 的源码利用这种保护机制
  • 封装私有变量

保存的应用

  • 选项卡闭包的解决方案。

事件绑定引发的索引问题,解决,略

# 5、原型和原型链

什么是原型?什么是原型链?如何理解?

原型:每个 JS 对象都有一个** proto ** 属性,这个属性指向了原型。

console.log([ ]);

length: 0

** proto ** : Array(0)

只要是对象类型,都会有** proto ** 属性, 这个属性指向也是一个原型对象,原型对象也存在一个 ** proto ** 属性。

原型链:原型链就是多个对象通过** proto **的方式连接起来

原型与构造函数的关系

原型

每个构造函数都有一个原型对象并通过 constructor 指向这个构造函数。

通过构造函数 new 的实例对象有隐式的** proto ** 指向原型对象

原型链与继承

根据上图中思考

当原型对象指向另一个类型的实例对象呢?

会发生以下的事情

在这里插入图片描述)

js 中一切即对象,会通过层层隐式查询,查询到 Object 这个对象的原型上面,并继承上面的属性方法

图中由__proto__属性组成的链子,就是原型链,原型链的终点就是**「null」**。


instanceof 的原理就是通过判断该对象的原型链中是否可以找到该构造类型的 prototype 类型

function Foo(){}
var f1 = new Foo();
console.log(f1 insatnceof Foo); // true

# 6、 JS 中的继承,比较优缺点

继承

继承的方式有哪些?以及各自继承方式的优缺点?

1、经典继承(构造函数)

funciton Father(){
    this.colors = ["red","blue","green"];
}

function Son(){
  Father.call(this);
}

let s = new Son();
console.log(s.colors)  // ["red","blue","green"]

基本思想:在子类的构造函数的内部调用父类的构造函数。

优点:

  • 保证了原型链中引用类型的独立,不被所有实例共享。
  • 子类创建的时候可以像父类进行传参。

缺点:

  • 继承的方法都在构造函数中定义,构造函数不能复用
  • 父类定义的方法对于子类型而言是不可见的

2、组合继承

function Father(name) {
  this.name = name;
  this.colors = ["red", "blue"];
}
// 方法定义在原型对象上(共享)
Father.prototyoe.sayName = function() {
  alert(this.name);
};
function Son(name, age) {
  // 子类继承父类属性
  Father.call(thisc, name);
  // this.age = age;
}
// 子类和父类共享的方法(实现了父类属性和方法的复用)
Son.prototype = new Father();
// 子类实例对象共享的方法
Son.prototype.sayAge = function() {
  alert(this.age);
};

var instance1 = new Son("louis", 5);
instance1.colors.push("black");
console.log(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //louis
instance1.sayAge(); //5

基本思想

  • 使用原型链实现对原型对象属性和方法的继承
  • 通过借用构造函数来实现对实例属性的继承

优点

  • 每个原型对象上定义的方法实现了函数的复用
  • 每个实例都有属于自己的属性

3、原型继承

function object(o) {
  function F() {}
  F.prototype = o;
  // 每次返回的new是不同的
  return new F();
}

var person = {
  friends: ["Van", "Louis", "Nick"]
};

// 实例 1
var anotherPerson = object(person);
anotherPerson.friends.push("Rob");

// 实例 2
var yetAnotherPerson = object(person);
yetAnotherPerson.friends.push("Style");

// 都添加至原型对象的属性(所共享)
alert(person.friends); // "Van,Louis,Nick,Rob,Style"

基本思想:创建临时构造函数,将传入的对象作为该构造函数的原型对象,返回这个新构造函数的实例

4、寄生式继承

function createAnother(original) {
  var clone = object(original); // 通过调用object函数创建一个新对象
  clone.sayHi = function() {
    alert("hi");
  };
  return clone; // 返回这个对象
}
  • 基本思想:不必为了制定子类型的原型而调用超类型的构造函数
  • 优点:寄生组合式继承就是为了解决组合继承中两次构造函数的开销

# 7、 JS 中的垃圾回收机制

什么事内存泄漏?为什么会导致内存泄漏?

: 不再用到的内存,没有及时释放,就叫做内存泄漏

JS 垃圾回收机制

运行原理?

找出不在继续使用的变量,然后释放其内存,按照固定的时间间隔,周期性的执行该垃圾回收操作

两种策略

  • 标记清除法
  • 引用计数法

如何管理内存

js 内存自动管理的,但是存在一些问题,分配给 web 浏览器的可用内存数量通常比分配给桌面应用程序的少。

为了更好的让页面获得好的性能,必须确保 js 变量占用最少的内存,最好的方式就是不用的变量引用释放掉,叫做解除引用。

  • 对于局部变量来说,函数执行完成离开环境变量,变量将自动解除
  • 对于全局变量我们需要进行手动解除
var a = 20; // 在对内存中给数值变量分配空间
alert(a + 100); // 使用内存
var a = null; // 使用完毕之后,释放内存空间

通过上边的垃圾回收机制的标记清除法的原理得知,只有与环境变量失去引用的变量才会被标记回收,所以将对象的引用设置为 null,此变量也就失去了引用,等待被垃圾回收器回收。

# 8、 JS 中的深拷贝,浅拷贝

什么是深拷贝?什么是浅拷贝?

因为数据类型分为基本类型和引用类型,对基本类型的拷贝就是对值的复制,但是对于引用类型来说,拷贝的不是值,而是值的地址,最终两个变量的地址指向的是同一个值

var obj1 = { name: "xiaohong" };
var obj2 = obj1;
obj2.name = "limi";
obj1; // {name : 'limi'}

要想将 obj1 和 obj2 的关系断开,也就是让他们不指向同一个地址,根据不同层次的拷贝,分为了深拷贝和浅拷贝

  • 浅拷贝: 只进行一层关系的拷贝。
  • 深拷贝:进行无线层次的拷贝。

浅拷贝和深拷贝分别如何实现的,有哪几种实现方式?

  • 1、浅拷贝
function shallowClone(o) {
  const obj = {};
  for (let i in o) {
    obj[i] = o[i];
  }
  return obj;
}
  • 2、扩展运算符实现
let a = { c: 1 };
let b = { ...a };
a.c = 2;
b.c; // 1
  • 3、Object.assign() 实现
let a = { c: 1 };
let b = Object.assign({}, a);
a.c = 2;
b.c; // 1
  • 4、深拷贝 ,在浅拷贝的基础上加上递归
var a1 = {b : {c: {d:1 }}}
function deepClone(source) {
  var target = {};
  for(var i in source) {
    if(source.hasOwnProperty(i) {
         if(typeof source[i] === 'object'){
             target[i] = deepClone(source[i])  // 递归
         }else{
           target[i] = source[i]
         }
     })
  }
   return target;
}

上面深拷贝存在问题

  • 参数没有校验
  • 判断对象不严谨
  • 递归层次深,容易爆栈
  • 循环引用问题
var a = {};
a.a = a;
deepClone(a); // 会造成一个死循环

两种解决循环引用问题的方法

  • 暴力破解
  • 循环检测
  • 5、利用 JSON.parse(JSON.stringify(object)) , 但是也有局限
function cloneJSON(source) {
  return JSON.parse(JSON.stringify(source));
}

这种方法来说,内部的原理实现也是使用的递归,递归到一定深度,也是会出现爆栈的问题,但是不会造成循环引用的问题,内部解决方案正式用到了循环检测

详细深拷贝实现:------实现中。。

# 9、 js 中的异步编程

由于 Javascript 是单线程的,单线程就是意味着阻塞问题,当一个任务执行完成之后才执行下一个任务,这样会导致出现页面卡死的状态,页面无响应,影响用户体验,所以不得不出现了同步和异步的解决方案。

JS 为什么是单线程?带来了哪些问题呢?

JS 单线程的特点是同一时刻只能执行一个任务,这是由一些与用户的互动以及操作一些 DOM 相关操作决定的。否则使用多线程会带来复杂得瑟同步问题,如果执行同步问题的话,多线程需要加锁,执行任务造成非常的繁琐。

虽然 HTML5 标准规定,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。

因为单线程会造成阻塞,为了解决这个问题,不得不涉及 JS 两种任务,分别为同步任务和异步任务

如何实现异步编程?

最早是使用回调函数,在特定的事件或条件发生时调用,如 Ajax 回调

// jQuery 中的ajax
$.ajax({
  type:'post',
  url:'test.json',
  dataType:'json',
  success: function(res) {
    // 响应成功的回调
  },
  fail:function(res){
    // 响应失败的huidiao
}
})

如果某个请求存在依赖另一个请求,就会形成不断的循环嵌套,我们称之为回调地狱。

异步中 try catch return 问题?

  • 为什么不能捕获异常?

    跟 js 的运行机制有关,异步任务执行完成会加入任务队列,当执行栈中没有可执行任务时,主线程才会取出任务队列中的异步任务入栈执行,但是当异步任务执行的时候,捕获异常的函数已经在执行栈中退出了,所以异常无法捕获。

  • 为什么不能 return?

return 只能终止回调的函数执行,而不能终止外部代码的执行

如何解决回调地狱问题呢?

ES6 给我们三种解决方案,分别是 Generator、Promise、async/awiat (ES7)

# 10、 执行上下文

异步代码的执行顺序? Event Loop 的运行机制是如何的运行的?

由上文已知 JS 是单线程并且通过使用同步和异步任务解决 JS 的阻塞问题,那么异步代码执行顺序,以及 EventLoop 是如何运作的呢?

深入事件循环机制,需要弄懂以下概念

  • 执行上下文
  • 执行栈
  • 微任务
  • 宏任务

执行上下文

抽象概念:可理解为代码执行的一个环境,

  • 全局执行上下文: 全局 this 指向 window,外部加载的 js 文件或本地< script >标签中的代码

  • 函数执行上下文: 函数调用时创建的新的局部上下文

  • Eval 执行上下文(不常用)

执行栈

执行栈就是我们数据结构中的"栈",它具有"先进后出"的特点,所以我们代码执行的时候,遇到一个执行上下文就将其依次压入执行栈中。

function foo() {
  console.log("a");
  bar();
  console.log("b");
}

function bar() {
  console.log("c");
}

foo();
// a
// c
// b
  • 初始化状态,执行栈任务为空
  • foo 函数执行,foo 进入执行栈,输出 a,碰到函数 bar
  • bar 进入执行栈,开始执行 bar,输出 c
  • bar 函数执行完出栈,继续执行执行栈顶端的函数 foo,最后输出 b
  • foo 出栈,所有执行栈内任务执行完毕

宏任务

对于宏任务一般包括

script;
setTimeout;
setInterval;
setImmediate;
I / O;

微任务

对于微任务一般包括:

Promise;
Process.nextTick;
MutationObserver;

运行机制

js 事件循环机制,js 中任务执行顺序都是靠函数调用栈来实现的。

  • 1、事件循环机制从 script 标签开始,整个 script 标签是作为一个宏任务处理的
  • 2、执行过程中,遇到宏任务,如 setTimeout 就将当前任务分发到对应的执行队列中去。
  • 3、遇到微任务,如 Promise,在创建实例对象时,代码顺序执行,遇到.then 操作,该任务会分发到微任务队列中去
  • 4、script 标签内的代码执行完毕后,同时执行过程中涉及的宏任务和微任务也分配到了相应的队列中去
  • 5、此时宏任务执行完毕,再去微任务队列找微任务
  • 6、微任务执行完毕,第一轮消息循环执行完毕,页面进行一次渲染
  • 7、第二轮消息循环,从宏任务中取出任务队列执行
  • 8、如果两个任务队列没有任务可执行了,所有任务执行完毕

演示

 <script>
        console.log('1');
        setTimeout(() => {
            console.log('2')
        }, 1000);
        new Promise((resolve, reject) => {
            console.log('3');
            resolve();
            console.log('4');
        }).then(() => {
            console.log('5');
        });
       console.log('6');  // 1、3、4、6、5、2
    </script>

简化步骤

  • 执行宏任务(script 中同步执行代码), 执行完毕, 调用栈为空
  • 检查微任务队列是否有可执行任务,执行完所有微任务
  • 进行页面渲染
  • 第二轮从宏任务队列去除一个宏任务执行,重复以上循环

参考:
来自原形与原型链的拷问 一文吃透所有 JS 原型相关知识点

《大前端吊打面试官系列》之原生 JavaScript 精华篇