知识梳理——JavaScript

all!!!

复习计划!!!

1. 说说前端中的事件流

HTML中与javascript交互是通过事件驱动来实现的,例如鼠标点击事件onclick、页面的滚动事件onscroll等等,可以向文档或者文档中的元素添加事件侦听器来预订事件。想要知道这些事件是在什么时候进行调用的,就需要了解一下“事件流”的概念。

什么是事件流:事件流描述的是从页面中接收事件的顺序.

DOM2级事件流包括下面几个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段

addEventListener:addEventListener 是DOM2 级事件新增的指定事件处理程序的操作,这个方法接收3个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。最后这个布尔值参数如果是true,表示在捕获阶段调用事件处理程序;如果是false,表示在冒泡阶段调用事件处理程序。默认是false

IE只支持事件冒泡。

2. 如何让事件先冒泡后捕获

在DOM标准事件模型中,是先捕获后冒泡。但是如果要实现先冒泡后捕获的效果,对于同一个事件,监听捕获和冒泡,分别对应相应的处理函数,监听到捕获事件,先暂缓执行,直到冒泡事件被捕获后再执行捕获事件。

什么是事件冒泡

事件冒泡:开始时由最具体的元素接收,然后逐级向上传播到到 DOM 最顶层节点。

js怎么阻止事件冒泡

1.标准写法:利用事件对象里面的stopPropagation()方法

var div = document.querySelector('div')
div.addEventListener('click', function(e) {
    e.stopPropagation()
})

2.非标准写法:IE6-8 利用事件对象cancelBubble属性

var div = document.querySelector('div')
div.addEventListener('click', function(e) {
     e.cancelBubble = true // cancel 取消 Bubble 泡泡
 })

3.说一下事件委托(事件代理)

简介:事件委托指的是,不在事件的发生地(直接dom)上设置监听函数,而是在其父元素上设置监听函数,通过事件冒泡,父元素可以监听到子元素上事件的触发,通过判断事件发生元素DOM的类型,来做出不同的响应。

举例:最经典的就是ul和li标签的事件监听,比如我们在添加事件时候,采用事件委托机制,不会在li标签上直接添加,而是在ul父元素上添加。

好处:比较合适动态元素的绑定,新添加的子元素也会有监听函数,也可以有事件触发机制。(减少内存消耗、动态绑定事件。)

什么是事件监听

addEventListener()方法,用于向指定元素添加事件句柄,它可以更简单的控制事件,语法为
element.addEventListener(event, function, useCapture);

  • 第一个参数是事件的类型(如 “click” 或 “mousedown”)

  • 第二个参数是事件触发后调用的函数

  • 第三个参数是个布尔值用于描述事件是冒泡还是捕获。该参数是可选的。事件传递有两种方式,冒泡和捕获,它定义了元素事件触发的顺序

如果将P元素插入到div元素中,用户点击P元素,在冒泡中,内部元素先被触发,然后再触发外部元素;在捕获中,外部元素先被触发,再触发内部元素

事件代理在捕获阶段的实际应用

可以在父元素层面阻止事件向子元素传播,也可代替子元素执行某些操作。

4.1 执行环境、作用域链、环境栈

1. 执行环境(又称执行上下文、作用域、环境)
执行环境:是js执行一段代码时的运行环境。执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。【该对象保存了变量提升的内容】

函数执行环境:每个函数都有自己的执行环境。当执行流进入一个函数时(即调用该函数),函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。

全局执行环境:是最外围的一个执行环境。全局执行环境被认为是window对象。

某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出 – 例如关闭网页或浏览器时,才会被销毁。)

作用域:就是在程序中定义变量和函数的可见范围,即作用域控制着变量和函数的可见性和生命周期。【大白话】

2. 作用域链
当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一致延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。
js经典面试题–变量提升、执行环境、作用域链

3.环境栈(又称调用栈)
1.在执行环境创建好后,JavaScript 引擎会将执行环境压入栈中,通常把这种用来管理执行环境的栈称为环境栈,又称调用栈。调用栈是 JavaScript 引擎追踪函数执行的一个机制。
2.调用栈是一种用来管理执行环境的数据结构,符合后进先出的规则

4.2 词法作用域、动态作用域(this)

1.词法作用域(静态的作用域)
词法作用域就是指作用域是由函数声明的位置来决定的,它是在代码编译阶段就决定好的,和函数是怎么调用的没有关系。(肉眼可见的函数外部的位置
JavaScript词法作用域

2.动态作用域(与this的调用有关)
动态作用域并不关心函数和作用域是如何声明以及在任何处声明的,只关心它们从何处调用。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套。

5.1 改变函数内部this指针的指向函数(bind,apply,call的区别)

相同点:

  • 改变 this 指向

不同点:

  • call 和 apply 是立刻执行的,而 bind 是返回了一个新的函数,这个函数不会马上执行,需要用()调用,且返回的函数还可以传入新的参数——支持柯里化形式传参 fn(1)(2)
  • call 和 apply 的第一个参数都是:要改变指向的对象。但第二个参数不同,call 可以传递多个参数,是arg1, arg2……这种形式;而 apply 是数组

实现 call

面试 |call, apply, bind的模拟实现和经典面试题

Function.prototype.MyCall = function (context, ...args) {
    // this 指的是 fn (是 fn 调用的 MyCall)
    console.log(this);

    // context 就是 传入的obj
    // 如果 context 为空,默认为 window (参考call的处理方式)
    context = context || window;
    // 在context上扩展一个 fn 方法 (将函数挂载到 context 上)
    context.fn = this;
    // 调用 context.fn,此时fn的 this 指向 contetx
    context.fn(...args);
    // 删除对象上的属性 (不然 fn 这个方法就会留在调用的对象上)
    delete context.fn;
    return
};

let name = "张三";
let obj = {
    name: '李四'
};

function fn() {
    console.log(this.name, ...arguments);
}

fn.MyCall(null);
fn.MyCall(obj, 11, 22);

// fn.call(null);
// fn.call(obj, 11, 22);

console.log(obj); // obj 上没有挂载 fn (已经delete了)

实现 apply

Function.prototype.MyApply = function (context, args) {
    context = context || window;
    context.fn = this;
    if(args) {
        context.fn(...args);
    } else {
        context.fn();
    }
    delete context.fn;
    return
};

let name = "张三";
let obj = {
    name: '李四'
};

function fn() {
    console.log(this.name, ...arguments);
}

fn.MyApply(null);
fn.MyApply(obj,[11,22]);

// fn.apply(null);
// fn.apply(obj,[11,22]);

实现 bind

  1. 函数调用,改变 this
  2. 返回一个绑定 this 的函数
  3. 接收多个参数
  4. 支持柯里化形式传参 fn(1)(2)
Function.prototype.MyBind = function (context, ...args) {
    // 获取调用 MyBind 的函数主体 (fn)
    const _this = this;
    // 返回一个函数
    // 因为支持柯里化形式传参,我们需要再次获取存储参数 (即第二次传入的参数)
    return (...otherArgs) => {
        _this.call(context, ...args.concat(...otherArgs))
    }
};

var obj = {
    name: '张三'
};

function fn() {
    console.log(this.name, ...arguments)
}

// 使用 MyBind 调用
fn.MyBind(obj, 12, 23, 34, 56)(88, 99);

var a = fn.MyBind(obj, 12, 23, 34, 56);
a();


// // 使用原生 bind 调用
// fn.MyBind(obj, 12, 23, 34, 56)(88, 99);
//
// var aa = fn.bind(obj, 12, 23, 34, 56);
// aa();

5.2 this的指向问题

你不知道的js中关于this绑定机制的解析[看完还不懂算我输]

四种绑定规则的优先级:显式绑定和new绑定无法直接比较(会报错)
显式绑定 > 隐式绑定 > 默认绑定
new绑定 > 隐式绑定 > 默认绑定

this指的是当前作用域的实例对象

  1. 默认绑定: 在非严格模式下,默认绑定的 this 指向 window,严格模式下 this 指向 undefined
  2. 隐式绑定: 函数在调用位置,是否有上下文对象,如果有,那么 this 就会隐式绑定到这个对象
  3. 显示绑定: 在某个对象上强制调用函数,从而将 this 绑定在这个对象上,我们可以通过apply/call/bind将函数中的 this 绑定到指定对象上
  4. new绑定: 使用构造函数调用时,this 会自动绑定在 new 创建的对象上
  5. 箭头函数: 继承最近一层非箭头函数的 this(箭头函数的 this 一旦绑定了上下文,就不会被任何代码改变)

5.3 this 的设计缺陷以及应对方案

1. 缺陷:嵌套函数中的 this 不会从外层函数中继承
2. 解决方案:

  • 把 this 保存为一个 self 变量,再利用变量的作用域机制传递给嵌套函数。
  • 继续使用 this,但是要把嵌套函数改为箭头函数,因为箭头函数没有自己的执行上下文,所以它会继承调用函数中的 this。

5.4 箭头函数与普通函数的区别

普通函数:
通过 function 关键字定义,this 指向取决于函数的调用方式

箭头函数:
1.使用" => "定义,语法更加简洁、清晰
2.箭头函数没有 prototype (原型),所以箭头函数本身没有this
箭头函数中的this是根据外层作用域来决定的,继承外层函数调用的this绑定

// 箭头函数
let a = () => {};
console.log(a.prototype); // undefined

// 普通函数
function a() {};
console.log(a.prototype); // {constructor:f}

3.箭头函数不会创建自己的this
箭头函数没有自己的this,箭头函数的this指向在定义(注意:是定义时,不是调用时)的时候继承自外层第一个普通函数的this。所以,箭头函数中 this 的指向在它被定义的时候就已经确定了,之后永远不会改变。

4.call | apply | bind 无法改变箭头函数中this的指向
call | apply | bind方法可以用来动态修改函数执行时this的指向,但由于箭头函数的this定义时就已经确定且永远不会改变。所以使用这些方法永远也改变不了箭头函数this的指向。

5.箭头函数不能作为构造函数使用

6.箭头函数不绑定arguments,取而代之用rest参数…代替arguments对象,来访问箭头函数的参数列表

箭头函数与普通函数的区别?

6.js的new操作符做了哪些事情

1.用new关键字新建了一个空对象
2.将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
3.在构造函数内使用this为新对象添加属性
4.如果函数没有返回其他对象,那么 new 表达式中的函数调用会返回这个新对象

实现new

// fn:构造函数
function _new(fn, ...arg) {
    // 创建一个新对象,使用现有的对象作为 新创建的对象的原型 (__proto__)
    const newObj = Object.create(fn.prototype);
    // 构造函数内部的 this 被赋值给这个新对象 (即 this 指向新对象)
    const obj = fn.apply(newObj, arg);
    // 如果构造函数返回一个对象,则返回该对象;否则,返回刚创建的新对象
    return obj instanceof Object ? obj : newObj;
}

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.speak = function () {
        console.log('my name is ' + this.name)
    };

    // 当构造函数 return 了一个对象时, 返回该对象
    // return {
    //     content: '333333'
    // }
    
    // 当构造函数 return null 时, 返回新对象(newObj)
    // 因为 null instanceof Object === false
    // return null
}

const Amy = _new(Person,"Amy", 19);
console.log(Amy);   // Person {name: "Amy", age: 19, speak: ƒ}

【1】构造函数通常不使用 return 关键字,它们通常初始化新对象,当构造函数的函数体执行完毕时,它会显式返回。在这种情况下,构造函数调用表达式的计算结果就是这个新对象的值。
【2】如果构造函数使用return语句但没有指定返回值,或者返回一个原始值,那么这时将忽略返回值,同时使用这个新对象作为调用结果。
【3】如果构造函数显式地使用return语句返回一个对象,那么调用表达式的值就是这个对象。

7.js的节流和防抖

【前端性能】高性能滚动 scroll 及页面渲染优化

节流:多次频繁触发时,函数执行一次后,只在大于规定周期后才会再次触发。
(页面滚动、DOM元素的拖拽、点击抢购)

防抖:多次频繁触发时,在规定周期内,只让最后一次生效。
(按钮点击太快、多次请求、窗口调整(resize)、搜索框实时联想)

防抖
 // 默认300ms的延时周期:如果在300ms之内再次输入,则重新触发此函数:1.先清除原有的定时器; 2.开启一个新的300ms的定时器,并在300ms之后执行
function debounce(fn, delay = 300) {
     let timer = null;
     return function (...args) {
         console.log(args);
         
 		 // 清除定时器
         if (timer) {
             clearTimeout(timer);
             timer = null;
         }

		 // 开启一个新的定时器
         timer = setTimeout(() => {
             fn.apply(this, args);
         }, delay);
     }
 }
    
  function demo() {
      console.log('防抖啦')
  }
  
  // 用句柄事件绑定调用dou事件,所以this为div节点对象
  document.querySelector('div').addEventListener('scroll', debounce(demo,  300))
//节流throttle代码:
function throttle(fn) {
    let canRun = true; // 通过闭包保存一个标记
    return function () {
         // 在函数开头判断标记是否为true,不为true则return
        if (!canRun) return;
         // 立即设置为false
        canRun = false;
        // 将外部传入的函数的执行放在setTimeout中
        setTimeout(() => { 
        // 最后在setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。
        // 当定时器没有执行的时候标记永远是false,在开头被return掉
            fn.apply(this, arguments);
            canRun = true;
        }, 500);
    };
}

function sayHi(e) {
    console.log(e.target.innerWidth, e.target.innerHeight);
}
window.addEventListener('resize', throttle(sayHi));

防抖和节流如何实现

8. 如何理解前端模块化

前端模块化就是把复杂的文件变成一个一个独立的模块,比如js文件等等,分成独立的模块有利于重用(复用性)和维护(版本迭代),但这样会引来模块之间相互依赖的问题,所以有了commonJS规范,AMD,CMD规范等等,以及用于js打包(编译等处理)的工具webpack

webpack和gulp区别(模块化与流的区别)

gulp强调的是前端开发的工作流程,我们可以通过配置一系列的task,定义task处理的事务(例如文件压缩合并、雪碧图、启动server、版本控制等),然后定义执行顺序,来让gulp执行这些task,从而构建项目的整个前端开发流程。

webpack是一个前端模块化方案,更侧重模块打包,我们可以把开发中的所有资源(图片、js文件、css文件等)都看成模块,通过loader(加载器)和plugins(插件)对资源进行处理,打包成符合生产环境部署的前端资源。

webpack用来干什么的

webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个bundle。

模块打包原理知道吗?

Webpack 实际上为每个模块创造了一个可以导出和导入的环境,本质上并没有修改代码的执行逻辑,代码执行顺序与模块加载顺序也完全一致。

什么是热更新

Hot Module Replacement是指当我们对代码修改并保存后,webpack将会对代码进行重新打包,并将新的模块发送到浏览器端,浏览器用新的模块替换掉旧的模块,以实现在不刷新浏览器的前提下更新页面。(相对于live reload刷新页面的方案,HMR的优点在于可以保存应用的状态,提高了开发效率)

说一说Loader和Plugin的区别?

Loader 本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。
因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对其他类型的资源进行转译的预处理工作。
(ts-loader / babel-loader / sass-loader / image-loader)

Plugin 就是插件,基于事件流框架 Tapable,插件可以扩展 Webpack 的功能,在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
(webpack-bundle-analyzer / Hot Module Replacement……)

Loader 在 module.rules 中配置,作为模块的解析规则,类型为数组。每一项都是一个 Object,内部包含了 test(类型文件)、loader、options (参数)等属性。

Plugin 在 plugins 中单独配置,类型为数组,每一项是一个 Plugin 的实例,参数都通过构造函数传入。

Loaders是在打包构建过程中用来处理源文件的(JSX,Scss,Less…),一次处理一个;插件并不直接操作单个文件,它直接对整个构建过程其作用。

「吐血整理」再来一打Webpack面试题

webpack和vite的区别

  1. vite是直接启动开发服务器,请求哪个模块再对该模块进行实时编译。而webpack会先打包,然后启动开发服务器,请求服务器时直接给予打包结果。
  2. 由于vite在启动的时候不需要打包,也就意味着不需要分析模块的依赖、不需要编译,因此启动速度非常快。当浏览器请求某个模块时,再根据需要对模块内容进行编译。这种按需动态编译的方式,极大的缩减了编译时间,项目越复杂、模块越多,vite的优势越明显。 在HMR方面,当改动了一个模块后,仅需让浏览器重新请求该模块即可,不像webpack那样需要把该模块的相关依赖全部编译一次
  3. 当需要打包到生产环境时,vite使用传统的rollup进行打包,因此,vite的主要优势在开发阶段。另外,由于vite利用的是ES Module,因此在代码中不可以使用CommonJS

9.实现一个once函数,传入函数参数只执行一次

function ones(func){
	var tag=true;
	return function(){
		if(tag==true){
			func.apply(null,arguments);
			tag=false;
		}
		return undefined
	}
}

10.闭包 有什么用

(1)什么是闭包:
闭包是指有权访问另外一个函数作用域中的变量的函数。当函数嵌套时,内层函数引用了外层函数作用域下的变量,并且内层函数在全局作用域下可访问时,就形成了闭包。

闭包就是函数的局部变量集合,只是这些局部变量在函数返回后会继续存在。闭包就是就是函数的“堆栈”在函数返回后并不释放,我们也可以理解为这些函数堆栈并不在栈上分配而是在堆上分配。当在一个函数内定义另外一个函数就会产生闭包。

(2)为什么要用闭包:

1、可以在全局作用域中读取函数内部的的变量
匿名自执行函数:我们知道所有的变量,如果不加上var关键字,则默认的会添加到全局对象的属性上去,这样的临时变量加入全局对象有很多坏处,比如:别的函数可能误用这些变量;造成全局对象过于庞大,影响访问速度(因为变量的取值是需要从原型链上遍历的)。除了每次使用变量都是用var关键字外,我们在实际情况下经常遇到这样一种情况,即有的函数只需要执行一次,其内部变量无需维护,可以用闭包。

2、可以让闭包中引用的变量始终保存在内存中。
结果缓存:我们开发中会碰到很多情况,设想我们有一个处理过程很耗时的函数对象,每次调用都会花费很长时间,那么我们就需要将计算出来的值存储起来,当调用这个函数的时候,首先在缓存中查找,如果找不到,则进行计算,然后更新缓存并返回值,如果找到了,直接返回查找到的值即可。闭包正是可以做到这一点,因为它不会释放外部的引用,从而函数内部的值可以得以保留。

封装:实现类和继承等。

深入理解:函数、匿名函数、自执行函数、闭包
什么是闭包?为什么要闭包?使用闭包应注意什么

11.能来讲讲JS的语言特性吗

运行在客户端浏览器上;

不用预编译,直接解析执行代码;

是弱类型语言,较为灵活;

与操作系统无关,跨平台的语言;

脚本语言、解释性语言

12. Js数据类型 和 数据类型的值

1. 数据类型
基本数据类型:Number、Boolean、String、Undefined、Null、(Symbol、BigInt)

复杂数据类型:Object(由一组无序的名值对组成的)

2. 数据类型的值
基本类型值: 是简单的数据段。7种基本类型是按值访问的,因为可以操作保存在变量中的实际值。
复制基本类型值时,两个变量的操作不会相互影响

引用类型值: 是保存在内存中的对象。js不能直接访问内存中的位置,也就是说不能直接操作对象的内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。为此,引用类型的值是按引用访问的。
复制引用类型值时,两个变量将引用同一个对象,他们之间会相互影响

3.引用类型
引用类型的值(对象)是引用类型的一个实例。引用类型是一种数据结构,用于将数据和功能组织在一起。
Object、Array、Date、RegExp、Function

栈和堆

栈内存:线性有序存储,容量小,系统分配效率高。(存放原始类型)
堆内存:首先要在堆内存新分配存储区域,之后又要把指针存储到栈内存中,效率相对就要低一些了。 (存放引用类型的值)

1.为什么一定要分“堆”和“栈”两个存储空间呢?所有数据直接存放在“栈”中不就可以了吗?
不可以的。这是因为JavaScript 引擎需要用栈来维护程序执行期间上下文的状态,如果栈空间大了话,所有的数据都存放在栈空间里面,那么会影响到上下文切换的效率,进而又影响到整个程序的执行效率。

2.垃圾回收
栈内存: 当函数执行结束,JS引擎通过向下移动ESP指针(记录调用栈当前执行状态的指针),来销毁该函数保存在栈中的执行上下文(变量环境、词法环境、this、outer)。

堆内存:标记清除引用计数

1.标记清除

  1. 垃圾收集器在运行时会给内存中的所有变量都加上一个标记
  2. 然后从各个根对象开始遍历,把还在被上下文变量引用的变量标记去掉标记
  3. 清理所有带有标牌机的变量,销毁并回收它们所占用的内存空间
  4. 最后垃圾回收程序做一次内存清理

使用标记清除策略的最重要的优点在于简单,无非是标记和不标记的差异。通过标记清除之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,这就造成出现内存碎片的问题。内存碎片多了后,如果要存储一个新的需要占据较大内存空间的对象,就会造成影响。对于通过标记清除产生的内存碎片,还是需要通过标记整理策略进行解决。【缺点:内存碎片化、分配速度慢】

2.标记整理
标记整理(Mark-Compact)算法 就可以有效地解决标记清除的两个缺点。它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存。

3.引用计次
引用计次: 当对象被引用的次数为零时进行回收,但是循环引用时,两个对象都至少被引用了一次,因此导致内存泄漏(垃圾:一般来说没有被引用的对象就是垃圾,就是要被清除, 有个例外如果几个对象引用形成一个环,互相引用,但根访问不到它们,这几个对象也是垃圾,也要被清除。)

3.V8对于垃圾回收机制的优化

V8 的垃圾回收策略主要基于分代式垃圾回收机制,V8 中将堆内存分为新生代和老生代两区域,采用不同的垃圾回收器也就是不同的策略管理垃圾回收。(V8 整个堆内存的大小就等于新生代加上老生代的内存)

新生代的对象为存活时间较短的对象,简单来说就是新产生的对象,通常只支持 1~8M 的容量,而老生代的对象为存活事件较长或常驻内存的对象,简单来说就是经历过新生代垃圾回收后还存活下来的对象,容量通常比较大。

1.新生代内存回收
对于新生代内存的回收,通常是通过Scavenge 的算法进行垃圾回收,就是将新生代内存进行一分为二,正在被使用的内存空间称为使用区,而限制状态的内存空间称为空闲区。

  1. 新加入的对象都会存放在使用区,当使用区快写满时就进行一次垃圾清理操作。
  2. 在开始进行垃圾回收时,新生代回收器会对使用区内的对象进行标记
  3. 标记完成后,需要对使用区内的活动对象拷贝到空闲区进行排序
  4. 而后进入垃圾清理阶段,将非活动对象占用的内存空间进行清理
  5. 最后对使用区和空闲区进行交换,使用区->空闲区,空闲区->使用区

新生代中的变量如果经过回收之后依然一直存在,那么会放入到老生代内存中,只要是已经经历过一次Scavenge算法回收的,就可以晋升为老生代内存的对象。

2.老生代内存回收
当然,Scavenge算法也有其适用场景范围,对于内存空间较大的就不适合使用Scavenge算法。此时应该使用Mark-Sweep(标记清除)和Mark-Compact(标记整理)的策略进行老生代内存中的垃圾回收。

首先是标记阶段,从一组根元素开始,递归遍历这组根元素,遍历过程中能到达的元素称为活动对象,没有到达的元素就可以判断为非活动对象。清除阶段老生代垃圾回收器会直接将非活动对象,也就是数据清理掉。

同样的标记清除策略会产生内存碎片,因此还需要进行标记整理策略进行优化。

Javascript的垃圾回收机制知多少?
「硬核JS」你真的了解垃圾回收机制吗

BigInt

在JS中,按照IEEE 754-2008标准的定义,所有数字都以双精度64位浮点格式表示。
在此标准下,无法精确表示的非常大的整数将自动四舍五入。确切地说,JS 中的Number类型只能安全地表示-9007199254740991 (-(2^53-1)) 和9007199254740991(2^53-1)之间的整数,任何超出此范围的整数值都可能失去精度。

Math.pow(2,53) === Math.pow(2,53) + 1    // true

BigInt 是一种新的数据类型,用于当整数值大于Number数据类型支持的范围时。这种数据类型允许我们安全地对大整数执行算术操作,表示高分辨率的时间戳,使用大整数id,等等,而不需要使用库。

13.四种判断数据类型的方法

1.typeof 适合基本类型和function类型的检测,无法判断null与object
2.instanceof 适合自定义对象,也可以用来检测原生对象,在不同的iframe 和 window间检测时失效,还需要注意Object.create(null)对象的问题
3.constructor 基本能判断所有类型,除了null和undefined,但是constructor容易被修改,也不能跨iframe使用
4.tostring能判断所有类型,可将其封装为全能的DataType()判断所有数据类型

1.typeof的返回值

typeof:是一元运算符,返回值为字符串,该字符串用来说明运算数的数据类型number、boolean、string、undefined、function、object

typeof null === 'object'
typeof [1,2,3] === 'object'
typeof {a:'1'} === 'object'
typeof Object === 'function'
typeof NaN === 'number'
typeof undefined === 'undefined'

2.instanceof
instanceof:用于判断某个变量是否是某个对象的实例,返回值为true或false
检测一个函数的原型是否存在与另一个对象的原型链当中,可以对不同的对象实例进行判断
(s instanceof Father)

// 利用 instanceof 判断基本数据类型
class CheckNumberType {
  static [Symbol.hasInstance](x) {
    return typeof x === 'number'
  }
};
console.log(100 instanceof CheckNumberType);  // true

3.constructor
每一个实例对象都可通过constructor来访问它的构造函数

'5'.__proto__.constructor === String // true
[5].__proto__.constructor === Array // true

undefined.__proto__.constructor // Cannot read property '__proto__' of undefined

null.__proto__.constructor // Cannot read property '__proto__' of undefined

4.toString
Object.prototype.toString方法返回对象的类型字符串,因此可用来判断一个值的类型。

Object.prototype.toString.call('5') // [object String]
Object.prototype.toString.call(5) // [object Number]
Object.prototype.toString.call([5]) // [object Array]
Object.prototype.toString.call(true) // [object Boolean]
Object.prototype.toString.call(undefined) // [object Undefined]
Object.prototype.toString.call(null) // [object Null]

面试题:四种判断数据类型的方法

14. null == undefined为什么

要比较相等性之前,不能将null 和 undefined 转换成其他任何值,但 null == undefined 会返回 true 。ECMAScript规范中是这样定义的。

null、undefined有什么区别

null:表示“没有对象”,即此处不应该有值(转为数值时为0)

  • 作为函数的参数,表示该函数的参数不是对象。
  • 作为对象原型链的终点。

undefined:表示"缺少值",就是此处应该有一个值,但是还没有定义(转为数值时为NaN)

  • 变量被声明了,但没有赋值时,就等于undefined。
  • 调用函数时,应该提供的参数没有提供,该参数等于undefined。
  • 对象没有赋值的属性,该属性的值为undefined。
  • 函数没有返回值时,默认返回undefined。

基本类型的计算

null + 5	//5
undefined + 4 	//NaN
null + true    //1
true + false   //1
5 + 6 + "6"  //"116"

15.暂停死区

在代码块内,使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”

16.AngularJS双向绑定原理

Angular将双向绑定转换为一堆watch表达式,然后递归这些表达式检查是否发生过变化,如果变了则执行相应的watcher函数(指view上的指令,如ng-bind,ng-show等或是{{}})。等到model中的值不再发生变化,也就不会再有watcher被触发,一个完整的digest循环就完成了。
Angular中在view上声明的事件指令,如:ng-click、ng-change等,会将浏览器的事件转发给$scope上相应的model的响应函数。等待相应函数改变model,紧接着触发脏检查机制刷新view。

watch表达式:可以是一个函数、可以是 s c o p e 上的一个属性名,也可以是一个字符串形式的表达式。 scope上的一个属性名,也可以是一个字符串形式的表达式。 scope上的一个属性名,也可以是一个字符串形式的表达式。watch函数所监听的对象叫做watch表达式。watcher函数:指在view上的指令(ngBind,ngShow、ngHide等)以及{{}}表达式,他们所注册的函数。每一个watcher对象都包括:监听函数,上次变化的值,获取监听表达式的方法以及监听表达式,最后还包括是否需要使用深度对比(angular.equals())

17.说一下什么是virtual dom

用 js 按照DOM结构来实现的树形结构对象

1.用js对象模拟DOM(虚拟DOM)
利用 createElement 方法创建 VNode,每个 VNode 有 children,children 每个元素也是一个 VNode,这样就形成了一个 VNode Tree,它很好的描述了我们的 DOM Tree
2.把此虚拟DOM转成真实DOM并插入页面中(render)
3.如果有事件发生修改了虚拟DOM,比较两颗虚拟DOM树的差异,得到差异对象(diff)
4.把差异对象应用到真正的DOM树上,视图就更新了(patch)

Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。

Virtual Dom 的优势在哪里?

其实这道题目面试官更想听到的答案不是上来就说「直接操作/频繁操作 DOM 的性能差」,如果 DOM 操作的性能如此不堪,那么 jQuery 也不至于活到今天。所以面试官更想听到 VDOM 想解决的问题以及为什么频繁的 DOM 操作会性能差。

首先我们需要知道:

DOM 引擎、JS 引擎 相互独立,但又工作在同一线程(主线程)JS 代码调用 DOM API 必须 挂起 JS 引擎、转换传入参数数据、激活 DOM 引擎,DOM 重绘后再转换可能有的返回值,最后激活 JS 引擎并继续执行若有频繁的 DOM API 调用,且浏览器厂商不做“批量处理”优化,引擎间切换的单位代价将迅速积累若其中有强制重绘的 DOM API 调用,重新计算布局、重新绘制图像会引起更大的性能消耗。

其次是 VDOM 和真实 DOM 的区别和优化:

1.虚拟 DOM 不会立马进行排版与重绘操作
2.虚拟 DOM 进行频繁修改,然后一次性比较并修改真实 DOM 中需要改的部分,最后在真实 DOM 中进行排版与重绘,减少过多DOM节点排版与重绘损耗
3.虚拟 DOM 有效降低大面积真实 DOM 的重绘与排版,因为最终与真实 DOM 比较差异,可以只渲染局部

19. JS中继承实现的几种方式

1、原型链继承,将父类的实例作为子类的原型,他的特点是实例是子类的实例也是父类的实例,父类新增的原型方法/属性,子类都能够访问,并且原型链继承简单易于实现,缺点是来自原型对象的所有属性被所有实例共享,无法实现多继承,无法向父类构造函数传参。
2、构造继承,使用父类的构造函数来增强子类实例,即复制父类的实例属性给子类,

构造继承可以向父类传递参数,可以实现多继承,通过call多个父类对象。但是构造继承只能继承父类的实例属性和方法,不能继承原型属性和方法,无法实现函数服用,每个子类都有父类实例函数的副本,影响性能

3、实例继承,为父类实例添加新特性,作为子类实例返回,实例继承的特点是不限制调用方法,不管是new 子类()还是子类()返回的对象具有相同的效果,缺点是实例是父类的实例,不是子类的实例,不支持多继承

4、拷贝继承:特点:支持多继承,缺点:效率较低,内存占用高(因为要拷贝父类的属性)无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)

5、组合继承:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用

6、寄生组合继承:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点

20. vue的生命周期

Vue实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模板、挂载Dom、渲染→更新→渲染、销毁等一系列过程,我们称这是Vue的生命周期。通俗说就是Vue实例从创建到销毁的过程,就是生命周期。
每一个组件或者实例都会经历一个完整的生命周期,总共分为三个阶段:初始化、运行中、销毁。

实例、组件通过new Vue() 创建出来之后会初始化事件和生命周期,然后就会执行beforeCreate钩子函数,这个时候,数据还没有挂载呢,只是一个空壳,无法访问到数据和真实的dom,一般不做操作

挂载数据,绑定事件等等,然后执行created函数,这个时候已经可以使用到数据,也可以更改数据,在这里更改数据不会触发updated函数,在这里可以在渲染前倒数第二次更改数据的机会,不会触发其他的钩子函数,一般可以在这里做初始数据的获取

接下来开始找实例或者组件对应的模板,编译模板为虚拟dom放入到render函数中准备渲染,然后执行beforeMount钩子函数,在这个函数中虚拟dom已经创建完成,马上就要渲染,在这里也可以更改数据,不会触发updated,在这里可以在渲染前最后一次更改数据的机会,不会触发其他的钩子函数,一般可以在这里做初始数据的获取

接下来开始render,渲染出真实dom,然后执行mounted钩子函数,此时,组件已经出现在页面中,数据、真实dom都已经处理好了,事件都已经挂载好了,可以在这里操作真实dom等事情…

当组件或实例的数据更改之后,会立即执行beforeUpdate,然后vue的虚拟dom机制会重新构建虚拟dom与上一次的虚拟dom树利用diff算法进行对比之后重新渲染,一般不做什么事儿

当更新完成后,执行updated,数据已经更改完成,dom也重新render完成,可以操作更新后的虚拟dom

当经过某种途径调用$destroy方法后,立即执行beforeDestroy,一般在这里做一些善后工作,例如清除计时器、清除非指令绑定的事件等等

组件的数据绑定、监听…去掉后只剩下dom空壳,这个时候,执行destroyed,在这里做善后工作也可以

21.简单介绍一下symbol

Symbol是ES6 的新增属性,代表用给定名称作为唯一标识,这种类型的值可以这样创建,let id=symbol(“id”)
Symbl确保唯一,即使采用相同的名称,也会产生不同的值,我们创建一个字段,仅为知道对应symbol的人能访问,使用symbol很有用,symbol并不是100%隐藏,有内置方法Object.getOwnPropertySymbols(obj)可以获得所有的symbol。
也有一个方法Reflect.ownKeys(obj)返回对象所有的键,包括symbol。

所以并不是真正隐藏。但大多数库内置方法和语法结构遵循通用约定他们是隐藏的

23.什么是js的闭包?有什么作用

MDN对闭包的定义是:闭包是指那些能够访问自由变量的函数,自由变量是指在函数中使用的,但既不是函数参数又不是函数的局部变量的变量,由此可以看出,闭包=函数+函数能够访问的自由变量,所以从技术的角度讲,所有JS函数都是闭包,但是这是理论上的闭包,还有一个实践角度上的闭包,从实践角度上来说,只有满足1、即使创建它的上下文已经销毁,它仍然存在,2、在代码中引入了自由变量,才称为闭包

闭包的应用:
1.模仿块级作用域。2、保存外部函数的变量。3、封装私有变量

24. JS的垃圾回收机制

GC(garbage collection),GC执行时,中断代码,停止其他操作,遍历所有对象,对于不可访问的对象进行回收,在V8引擎中使用两种优化方法,
1、分代回收,2、增量GC,目的是通过对象的使用频率,存在时长来区分新生代和老生代对象,多回收新生代区,少回收老生代区,减少每次遍历的时间,从而减少GC的耗时

回收方法:

1.引用计次,当对象被引用的次数为零时进行回收,但是循环引用时,两个对象都至少被引用了一次,因此导致内存泄漏
垃圾:一般来说没有被引用的对象就是垃圾,就是要被清除, 有个例外如果几个对象引用形成一个环,互相引用,但根访问不到它们,这几个对象也是垃圾,也要被清除。)

2.标记清除

js中最常用的垃圾回收方式就是标记清除。当变量进入环境时,例如,在一个函数中声明一个变量,就将这个变量标记为"进入环境",从逻辑上讲,永远不能释放进入环境变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为"离开环境"。

垃圾回收机制在运行的时候会给存储在内存中的所有变量都加上标记(可以是任何标记方式),然后,它会去掉处在环境中的变量及被环境中的变量引用的变量标记(闭包)。而在此之后剩下的带有标记的变量被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后垃圾回收机制到下一个周期运行时,将释放这些变量的内存,回收它们所占用的空间。

内存泄漏

对于持续运行的服务进程,必须及时释放内存,否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。

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

25.写一个newBind函数,完成bind的功能

bind()方法,创建一个新函数,当这个新函数被调用时,bind()的第一个参数将作为它运行时的this,之后的一序列参数将会在传递的实参前传入作为它的参数

Function.prototype.bind2 = function (context) {
	if (typeof this !== "function") {
		throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
	}
	var self = this;
	var args = Array.prototype.slice.call(arguments, 1);
	var fNOP = function () {};
	var fbound = function () {
		self.apply(this instanceof self ? this : context,args.concat(Array.prototype.slice.call(arguments)));
	}
	fNOP.prototype = this.prototype;
	fbound.prototype = new fNOP();
	return fbound;
}

26.浏览器的事件循环机制(Eventloop)

1.事件循环机制
概念:主线程从“任务队列”中读取事件,这个过程是循环不断的,所以整个的这个运行机制又称为Event Loop(事件循环)。

  1. 所有同步任务都在主线程上执行,形成一个执行栈。
  2. 主线程之外,还存在一个“任务队列”。只要异步任务有了运行结果,就在“任务队列”之中放置一个事件。
  3. 一旦“执行栈”中的所有同步任务执行完毕,系统就会读取“任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。

只要主线程空了,就会去读取“任务队列”,这就是JavaScript的运行机制。这个过程会不断重复。

2.宏任务和微任务
macrotasks: setTimeout, setInterval, setImmediate, I/O, UI rendering
microtasks: process.nextTick, Promise.then, MutationObserver

27. 介绍一下promise

1.Promise是一个构造函数,是异步编程的一种解决方案。能够简化层层回调,解决回调地狱。

2.它有pending、fulfilled、rejected三种状态,只有异步操作的结果可以决定当前状态,状态一旦改变,就不会再变了。 [resolve / reject]

3.Promise.all([p1, p2, p3]).then(res => {console.log(res)});
三个异步操作并行执行
所有的异步操作的结果放进一个数组,可以在一个回调中处理所有的返回数据

p的状态由p1、p2、p3决定,分成两种情况:

  • 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
  • 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

4.Promise.rase([p1, p2, p3]).then(res => {console.log(res)});
谁先执行完成就先执行回调(只执行一次)
先执行完的不管是成功还是失败,其余的都不再进入任何回调

5.异常处理
try-catch 捕捉不到 promise内部的错误, 但是抛出的错误可以通过catch来捕捉,所以在 promise 的尾部必须要有个 catch 接着

// try {
    let p = new Promise((resolve, reject) => {
        throw new Error("I'm error");
        // reject(new Error("I'm Error"));
    });
// }catch(e) {
//     console.log('catch',e);
// }

p.catch(result => {
    console.log(result);
});

6.不足
Promise虽然一方面解决了callback的回调地狱,但是相对的把回调“纵向发展”了,形成了一个回调链

async / await

1.含义
是 Generator 函数的语法糖,async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。

2.规则
async 返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。
await 表示在这里等待 promise 返回结果后,再继续执行, await 后面跟着的应该是一个 promise 对象(await命令就是内部then命令的语法糖)

3.错误处理
不需要.then/.catch, 可以直接用标准的 try/catch 语法进行错误捕捉

Promise和Async/Await用法整理

29.异步加载js的方法

defer:只支持IE,如果您的脚本不会改变文档的内容,可将 defer 属性加入到script标签中,以便加快处理文档的速度。因为浏览器知道它将能够安全地读取文档的剩余部分而不用执行脚本,它将推迟对脚本的解释,直到文档已经显示给用户为止。

async,HTML5属性仅适用于外部脚本,并且如果在IE中,同时存在defer和async,那么defer的优先级比较高,脚本将在页面完成时执行。

创建script标签,插入到DOM中

页面渲染时js阻塞的解决方法

1.一个包含外部样式表文件和外部脚本文件的HTML载入和渲染过程
1.浏览器下载HTML文件并开始解析DOM。
2.遇到样式表文件link[rel=stylesheet]时,将其加入资源文件下载队列,继续解析DOM。
3.遇到脚本文件时,暂停DOM解析并立即下载脚本文件。
4.下载结束后立即执行脚本,在脚本中可访问当前script标签以上的DOM。
5.脚本执行结束,继续解析DOM。
6.整个DOM解析完成,触发DOMContentLoaded事件。

2.什么是阻塞?
在页面中我们通常会引用外部文件,而浏览器在解析HTML页面是从上到下依次解析、渲染,如果head中引用了一个a.js文件,而这个文件很大或者有问题,需要2秒加载,那么浏览器会停止渲染页面(此时是白屏显示,就是页面啥都没有),2秒后加载完成才会继续渲染,这个就是阻塞。

3.为什么会阻塞?
因为浏览器不知道a.js中执行了哪些脚本,会对页面造成什么影响,所以浏览器会等js文件下载并执行完成后才继续渲染,如果这个时间过长,会白屏。

4.解决方法
1、推迟加载(延迟加载)
  如果页面初始的渲染并不依赖于js或者CSS可以用推迟加载,就是最后在加载js和css,把引用外部文件的代码写在最后。比如一些按钮的点击事件,比如轮播图动画的脚本也可以放在最后。
  
2、defer延迟加载
在文档解析完成开始执行,并且在DOMContentLoaded事件之前执行完成,会按照他们在文档出现的顺序去下载解析。效果和把script放在文档最后之前是一样的。
注:defer最好用在引用外部文件中使用,用了defer不要使用document.write()方法;使用defer时最好不要请求样式信息,因为样式表可能尚未加载,浏览器会禁止该脚本等待样式表加载完成,相当于样式表阻塞脚本执行。


3、异步加载
async异步加载:就是告诉浏览器不必等到加载完外部文件,可以边渲染边下载,什么时候下载完成什么时候执行。(async属性可以保证脚本下载的同时,浏览器继续渲染。需要注意的是,一旦采用这个属性,就无法保证脚本的执行顺序。哪个脚本先下载结束,就先执行那个脚本。另外,使用async属性的脚本文件里面的代码,不应该使用document.write方法。)


页面渲染时js阻塞的解决方法

5.defer属性和async属性到底应该使用哪一个?
一般来说,如果脚本之间没有依赖关系,就使用async属性,如果脚本之间有依赖关系,就使用defer属性。如果同时使用async和defer属性,后者不起作用,浏览器行为由async属性决定。

30.对象深度克隆的简单实现

deepClone(source: any){
    const targetObj = source.constructor === Array ? [] : {}; // 创建对象:并判断复制的目标是数组还是对象
    for(let keys in source){ // 遍历目标
        if(source[keys] && typeof source[keys] === 'object'){ // 如果值是对象,就递归一下
            //@ts-ignore
            targetObj[keys] = source[keys].constructor === Array ? [] : {};
            //@ts-ignore
            targetObj[keys] = this.deepClone(source[keys]);
        }else{ // 如果不是,就直接赋值
            //@ts-ignore
            targetObj[keys] = source[keys];
        }
    }
    return targetObj;
}

ES5的常用的对象克隆的一种方式。注意数组是对象,但是跟对象又有一定区别,所以我们一开始判断了一些类型,决定newObj是对象还是数组~

31.实现一个两列等高布局,讲讲思路

为了实现两列等高,可以给每列加上 padding-bottom:9999px;
margin-bottom:-9999px;同时父元素设置overflow:hidden;

32.数组去重

法一:indexOf循环去重

法二:ES6 Set去重;Array.from(new Set(array))

法三:Object 键值对去重;把数组的值存成 Object 的 key 值,比如 Object[value1] = true,在判断另一个值的时候,如果 Object[value2]存在的话,就说明该值是重复的。

33.性能优化

减少HTTP请求

使用内容发布网络(CDN)

添加本地缓存

压缩资源文件

将CSS样式表放在顶部,把javascript放在底部(浏览器的运行机制决定)

避免使用CSS表达式

减少DNS查询

使用外部javascript和CSS

避免重定向

图片lazyLoad

34.重排和重绘,讲讲看

重绘(repaint或redraw):当盒子的位置、大小以及其他属性,例如颜色、字体大小等都确定下来之后,浏览器便把这些原色都按照各自的特性绘制一遍,将内容呈现在页面上。重绘是指一个元素外观的改变所触发的浏览器行为,浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。
触发重绘的条件:改变元素外观属性。如:color,background-color等。

注意:table及其内部元素可能需要多次计算才能确定好其在渲染树中节点的属性值,比同等元素要多花两倍时间,这就是我们尽量避免使用table布局页面的原因之一。

重排(重构/回流/reflow):当渲染树中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建, 这就称为回流(reflow)。每个页面至少需要一次回流,就是在页面第一次加载的时候。

重绘和重排的关系:在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘。所以,重排必定会引发重绘,但重绘不一定会引发重排。

35.跨域的原理

跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript实施的安全限制,那么只要协议、域名、端口有任何一个不同,都被当作是不同的域。跨域原理,即是通过各种方式,避开浏览器的安全限制。

37.js原型链,原型链的顶端是什么?Object的原型是什么?Object的原型的原型是什么?在数组原型链上实现删除数组重复数据的方法

一文详解-es5原型和es6-class

能够把这个讲清楚弄明白是一件很困难的事,
首先明白原型是什么,在ES6之前,JS没有类和继承的概念,JS是通过原型来实现继承的,在JS中一个构造函数默认带有一个prototype属性,这个的属性值是一个对象,同时这个prototype对象自带有一个constructor属性,这个属性指向这个构造函数,同时每一个实例都会有一个_proto_属性指向这个prototype对象,我们可以把这个叫做隐式原型,我们在使用一个实例的方法的时候,会先检查这个实例中是否有这个方法,没有的话就会检查这个prototype对象是否有这个方法,

基于这个规则,如果让原型对象指向另一个类型的实例,即constructor1.protoytpe=instance2,这时候如果试图引用constructor1构造的实例instance1的某个属性p1,

首先会在instance1内部属性中找一遍,

接着会在instance1.proto(constructor1.prototype)即是instance2中寻找p1

搜寻轨迹:instance1->instance2->constructor2.prototype……->Object.prototype;这即是原型链,原型链顶端是Object.prototype

1.原型
只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。

在默认情况下,所有的原型对象都会自动获得一个constructor(构造函数)属性,这个属性是一个指向prototype属性所在函数的指针。

当调用构造函数创建一个新实例后,该实例内部将包含一个指针([[Prototype]]),指向构造函数的原型对象。(实例与构造函数没有直接的关系)

2.原型链
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而每个实例都包含一个指向原型对象的内部指针。 如果让原型对象等于另一个类型的实例(子类的原型对象=父类的实例),此时的原型对象将包含一个指向另一个原型的指针,层层递进,就构成了原型链。原型链顶端是Object.prototype

39. 简单讲一讲ES6的一些新特性

ES6在变量的声明和定义方面增加了let、const声明变量,有局部变量的概念,赋值中有比较吸引人的结构赋值,同时ES6对字符串、 数组、正则、对象、函数等拓展了一些方法,如字符串方面的模板字符串、函数方面的默认参数、对象方面属性的简洁表达方式,ES6也 引入了新的数据类型symbol,新的数据结构set和map,symbol可以通过typeof检测出来,为解决异步回调问题,引入了promise和 generator,还有最为吸引人了实现Class和模块,通过Class可以更好的面向对象编程,使用模块加载方便模块化编程,当然考虑到 浏览器兼容性,我们在实际开发中需要使用babel进行编译
重要的特性:

块级作用域:ES5只有全局作用域和函数作用域,块级作用域的好处是不再需要立即执行的函数表达式,循环体中的闭包不再有问题

rest参数:用于获取函数的多余参数,这样就不需要使用arguments对象了,

promise:一种异步编程的解决方案,比传统的解决方案回调函数和事件更合理强大

模块化:其模块功能主要有两个命令构成,export和import,export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能

40.如何使不同页面之间进行通信

41 说一下图片的懒加载和预加载

预加载:提前加载图片,当用户需要查看时可直接从本地缓存中渲染。
懒加载:懒加载的主要目的是作为服务器前端的优化,减少请求数或延迟请求数。

两种技术的本质:两者的行为是相反的,一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。

前端性能优化之图片懒加载

42.for in和for of的区别

for … of是作为ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构并且返回各项的值,和ES3中的for … in的区别如下

  1. for … of只能用在可迭代对象上(不能遍历普通对象),获取的是迭代器返回的value值,for … in 可以获取所有对象的键名
  2. for … in会遍历对象的整个原型链,性能非常差不推荐使用,而for … of只遍历当前对象不会遍历它的原型链
  3. 对于数组的遍历,for … in会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for … of只返回数组的下标对应的属性值
// 类数组
let obj2 = {0:'name', 1: 'color', 2: 'age',length:3}
let arr2 = Array.from(obj2) 					 // arr2:["name", "color", "age"]

for…of 怎么遍历普通对象:

1.一种解决方法是,使用Object.keys方法将对象的键名生成一个数组,然后遍历这个数组。

for (var key of Object.keys(someObject)) {  // 遍历键名
  console.log(key + ': ' + someObject[key]);
}

2.另一个方法是使用 Generator 函数将对象重新包装一下。

let obj = {a:'1', b:'2'}

function* entries(obj) {
  for (let key of Object.keys(obj)) {
    yield [key, obj[key]];
  }
}
for (let [key, value] of entries(obj)) {
  console.log(key, '->', value);
}
// a -> 1
// b -> 2

43. 0.1+0.2===0.3?

js使用 Number 类型来表示数字(整数或者浮点数),遵循 IEEE 754 标准,通过64位来表示一个数字(1 + 11 +52)

在两数相加时,计算机无法直接对十进制的数字进行运算(这是硬件物理特性已经决定的),会先转化成 二进制。0.1和0.2转换成二进制后会无限循环, 但是由于IEEE 754尾数位数限制,需要将后面多余的位截掉, 这样在进制之间的转换中精度已经损失。

0.1 -> 0.0001100110011001...(无限循环)
0.2 -> 0.0011001100110011...(无限循环)

0.1 + 0.2不等于0.3?为什么JavaScript有这种“骚”操作?

44.从输入 URL 到页面展示,这中间发生了什么?

1,用户输入url并回车
2,浏览器进程检查url,组装协议,构成完整的url
	2.1 如果是搜索内容,会使用浏览器默认的搜索引擎,加上搜索内容合成url
	2.2 如果输入的内容符合url规则,会加上协议,合成完整的url
3,浏览器进程通过进程间通信(IPC),把url请求发送给网络进程
4,网络进程接收到url请求后,检查本地缓存是否缓存了该请求资源,如果有则将该资源返回给浏览器进程
5,如果没有,网络进程向web服务器发起http请求(网络请求),请求流程如下:
    5.1 进行DNS解析,获取服务器ip地址、端口号
    5.2 利用ip地址和服务器建立tcp连接(3次握手)
    5.3 建立连接后,浏览器构建数据包(包含请求行、请求头、请求正文,并把该域名相关的Cookie等数据附加到请求头),然后向服务器发送请求消息
    5.4 服务器接收到消息后,根据请求信息构建响应数据(包括响应行、响应头、响应正文),然后发送回网络进程
    5.5 网络进程接收到响应数据后,进行解析
6,网络进程解析响应流程,检查状态码
    6.1 如果是301/302,则需要重定向,从Location自动中读取地址,重新进行第4步
    6.2 如果是200,则继续处理请求,检查响应类型Content-Type:
        a. 如果是下载类型,则将该请求提交给浏览器的下载管理器,同时该url请求的导航流程就此结束
        b. 如果是html,则浏览器就要准备渲染页面了。
7,准备渲染进程
    7.1 浏览器进程检查当前url是否和之前打开的渲染进程同一站点(根域名、协议是否相同)如果相同,则复用原来的进程,如果不同,则开启新的渲染进程
8,传输数据、更新状态
    8.1 当浏览器进程接收到网络进程的响应头数据之后,便向渲染进程发起“提交文档”的消息;
    8.2 渲染进程接收到“提交文档”的消息后,会和网络进程建立传输数据的“管道”;
    8.3 等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程;		  
    8.4 浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面  
9,渲染阶段:一旦文档被提交,渲染进程便开始页面解析和子资源加载了  
    9.1 渲染进程将 HTML 内容转换为能够读懂的 DOM 树结构。
    9.2 渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。
    9.3 创建布局树,并计算元素的布局信息。
    9.4 对布局树进行分层,并生成分层树。
    9.5 为每个图层生成绘制列表,并将其提交到合成线程。
    9.6 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
    9.7合成线程发送绘制图块命令 DrawQuad 给浏览器进程。
    9.8 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。

45. import 和 require 的区别

require、exports/module.exportsCommonJS 的标准,通常适用范围如 Node.js
require 是赋值过程并且是运行时才执行,也就是同步加载
require 可以理解为一个全局方法,因为它是一个方法所以意味着可以在任何地方执行。

import、export ES6 的标准,通常适用范围如 React
import 是解构过程并且是编译时执行,理解为异步加载
import 会提升到整个模块的头部,具有置顶性,但是建议写在文件的顶部。

require 的性能相对于 import 稍低。(因为 require 是在运行时才引入模块并且还赋值给某个变量,而 import 只需要依据 import 中的接口在编译时引入指定模块,所以性能稍高)

46.Set 和 Map

1.Set
类似于数组,但是成员的值都是唯一的,没有重复的值。

2.Map
它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。

3.WeakSet
WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别:

  1. WeakSet 的成员只能是对象,而不能是其他类型的值。
  2. WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。

4.WeakMap
WeakMap结构与Map结构类似,也是用于生成键值对的集合。它与Map的区别有两点:

  1. WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
  2. WeakMap的键名所指向的对象,不计入垃圾回收机制。(只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。)

47.js怎么判断一个空对象

1.空对象对应的字符串为 “{}”

JSON.stringify({}) == "{}"   // true

2.Object.getOwnPropertyNames()
通过 Object.getOwnPropertyNames 方法,获取到对象中的属性名(返回的是数组对象),我们可以通过判断数组的 length 来判断此对象是否为空:

var data = {};
var arr = Object.getOwnPropertyNames(data);  // []
console.log(arr.length === 0); // true

3.通过 ES6 的 Object.keys()
此方法也是返回对象中属性名组成的数组。

var data = {};
var arr = Object.keys(data);  // []
console.log(arr.length === 0); // true

48.js遍历数组的方式

  1. for、forEach、map
  • map()不会修改原数组,而是返回一个修改后的新数组;foreach()会修改原数组,即没有返回值
  • 导致foreach()的执行速度一般比map()快
  • 性能:for > forEach > map
  1. 如果map中途return,返回的是什么?
    知识梳理——JavaScript_第1张图片

你可能感兴趣的:(js,面试,javascript)