「2024」前端高频面试题之JS篇(二)

『前言』: 近期在梳理前端相关的高频面试题,参考文献:高程3、高程4、w3c、MDN等,分享出来一起学习。如有问题,欢迎指正。持续更新中~

内容共分为:html、css、js、ES6、ts、vue、小程序、git、网络请求相关,本篇内容是: 「2024」前端高频面试题之JS篇(二)

前端面试题系列文章:
【1】[「2024」前端高频面试题之HTML&CSS篇]
【2】[「2024」前端高频面试题之JS篇(一)]
【3】[「2024」前端高频面试题之JS篇(二)]
持续更新中~

目录

6,对原型、原型链的理解

1,prototype(原型)

2,都哪些值是函数数据类型:

3,constructor(构造函数)

4,都有哪些值是对象:

5,__proto__/[[prototype]](实例对象的__proto__原型指向所属类的prototype原型)

6,原型链

7,总结:

7,对执行上下文、作用域、作用域链的理解

1,执行上下文

1,JS代码运行的底层机制:

2,执行上下文EC(Execution Context)

2,作用域

Scope(作用域)当前的执行上下文,值和表达式在其中可见或可被访问。如果一个变量或表达式不在当前的作用域中,那么它是不可用的。作用域也可以堆叠成层次结构,子作用域可以访问父作用域,反过来则不行

3,作用域链

4,作用域面试题

5,作用域链面试题

8,undefined、null的区别

1,undefined

2,null

3,undefined 和 null的区别

9,this

1,给元素的某个事件绑定方法,当事件触发方法执行的时候,方法中的this是当前操作的元素本身

2,方法执行,看方法前面是否有点,有点, 点前面是谁就是谁,没有点,this是window(严格模式下是undefined)。自执行函数中的this是window和undefined

3,在构造函数模式执行中,函数体内的this是当前类的实例

4,箭头函数中没有自己的this,它里面的this是继承函数所处上下文中的this(所以真实项目中,一旦涉及this问题,箭头函数慎用)

5,案例

10,hasOwnProperty是什么?

1,hasOwnProperty和in的区别

2,怎么判断是不是自己的私有属性

11,科普什么是进程、内存、CPU、线程、JS的执行机制、同步、异步、事件循环

1,进程

浏览器的工作方式

并发

2,内存

3,CPU

4,线程

5,JS 的执行机制

6,同步

7,异步

8,事件循环

12,事件及浏览器常用的事件行为

1,事件是什么

2,事件绑定是什么

3,常见的事件行为

1,鼠标事件

2,键盘事件

3,移动端手指事件

4,表单元素常用事件

5,音视频常用事件

6,其他常用事件

4,DOM0和DOM2事件绑定的区别

5,事件对象

6,阻止事件的默认行为

7,事件的传播机制

8,mouseover 和mouseenter的本质区别

13,浏览器常用的事件-事件委托

1,事件委托

1,事件冒泡

2,事件捕获

3,事件流

4,所谓的事件委托是什么?

5,什么时候用事件委托,用它的好处?

6,事件对象的ev.target事件源

2,事件委托的意义

1,基于事件委托实现,整体性能要比一个个的绑定方法高出50%左右

2,如果多元素触发,业务逻辑属于一体的,基于事件委托来处理,更加的好

3,某些业务场景只能基于事件委托来处理

3,案例

1,购物车案例

2,追加按钮点击并弹出相应的li的索引案例

14,var、let、const的区别

1,区别总结

2,区别详细介绍

1,块级作用域

2,变量提升

3,暂时性死区

4,不允许重复声明

5,const声明的变量可以被修改吗

3,经典面试题

15,JS文件放在head里还是body里,什么区别?

1,JS文件 在 body 元素中

2,async、defer


【前言】:近期刷面试题时候,总觉得应该梳理出一份清晰且相对全面的前端面试题供自己复习和巩固,其特点是每道题的答案我都会查阅百度百科、官方、查阅多篇博客加上自己的见解进行总结归纳,所以如果有不对的地方,希望可以提出来我会及时改正。

内容共分为:html、css、js、ES6、ts、vue、小程序、git、网络请求相关,本篇内容是JS(6~15题,接上篇文章)

6,对原型、原型链的理解

1,prototype(原型)

每一个函数数据类型的值,都有一个自带的属性:prototype(原型),这个属性的属性值是一个对象,这个对象用来存储实例公用的属性和方法

原型:每一个函数数据类型都有一个天生自带的prototype,这个prototype就叫原型

2,都哪些值是函数数据类型:
  • 一个普通的函数

  • 类也是函数数据类型的
    • 类分为自定义类和内置类
      • 内置类比如Number、Array

3,constructor(构造函数)

prototype这个对象上有一个自带的属性叫constructor(构造函数),这个属性存储的是当前函数本身

Fn.prototype.constructor === Fn  => true

每一个对象数据类型的值也有一个天生自带的属性叫__proto__下划线下划线proto下划线下划线,这个属性指向所属类的原型prototype

4,都有哪些值是对象:
  • 普通对象、数组、正则、Math(数学函数对象)、日期、类数组等

  • 实例也是对象

  • 函数的原型prototype属性的值也是个对象

  • 函数也是对象

5,__proto__/[[prototype]](实例对象的__proto__原型指向所属类的prototype原型)

每一个对象数据类型的值也有一个自带的属性叫__proto__,这个属性指向所属类的原型prototype

function Fn() {
  /**
       知识点扩展:new 执行会把类当作普通函数执行(也有类执行的一面)
        - 1,创建一个私有的栈内存
        - 2,形参赋值 & 变量提升
        - 3,浏览器创建一个对象出来,这个对象就是当前类的一个新实例,并且让函数中的this指向这个实例对象,即构造函数模式中,方法中的this是当前类的实例
        - 4,代码执行
        - 5,在我们不设置return的情况下,浏览器会把创建的实例对象默认返回
    */
  this.x = 100;
  this.y = 200;
}
let f1 = new Fn();
let f2 = new Fn();

以上代码给函数的原型加个公有的方法:

function Fn() {
  this.x = 100;
  this.y = 200;
}
Fn.prototype.eat = function () {};
Fn.prototype.say = function () {};
let f1 = new Fn();
let f2 = new Fn();

6,原型链

原型链:先找自己私有的属性和方法,找到直接调用,找不到继续基于__proro__的所属类的原型(Fn.prototype)查找相应的属性和方法....直到找到Object这个类的基类的原型(Object.prototype)为止

原型链查找机制:1,先找自己私有的属性,有则调用,没有继续找 2,基于__proto__ 找所属类原型上的方法(Fn.prototype),如果没有则继续找....,一直找到Object.prototype为止,这种查找机制就叫做原型链查找机制

7,总结:
  • 1, 每一个函数数据类型都有一个天生自带的属性叫prototype(原型属性),这个属性的属性值是一个对象用来存储当前类的公用属性和方法的

  • 2,每一个类原型对象上有一个天生自带的属性叫constructor,存储的是当前函数或当前类本身

  • 3,每一个对象都天生自带一个属性叫__proto__,这个属性指向所属类的prototype这个原型 当我们通过实例调取某一个属性的时候,首先看看是不是私有属性,是私有的就拿私有的,如果不是私有的,默认通过__proto__往所属类的原型上找,如果没有的话,再通过原型上的__proto__再往上找...直到找到Object这个类的基类的原型(Object.prototype)为止,这个机制叫原型链查找机制

7,对执行上下文、作用域、作用域链的理解

1,执行上下文

执行上下文可以简称上下文。

每一个上下文都有一个关联的变量对象(VO),而这个上下文中定义的所有变量和函数都存在于这个对象上,虽然无法通过代码访问变量对象,但后台处理数据会用到它。

全局上下文是最外层的上下文。在浏览器中,全局上下文就是我们常说的window对象,因此所有通过var定义的全局变量和函数都会成为window对象的属性和方法。使用let和const的顶级声明不会定义在全局上下文中,但是在作用域链解析上效果是一样的。上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或者退出浏览器)

每个函数调用都会有自己的上下文。当代码执行流入函数的时候,函数的上下文被推到一个上下文栈上。在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还之前的执行上下文。

var color = "pink";
function changeColor() {
  if (color === "pink") {
    color = "red";
  } else {
    color = "blue";
  }
}
changeColor();

以上代码,函数changeColor()的作用域链包含两个对象:一个是它自己的变量对象,另一个是全局上下文的变量对象。

这个函数内部之所以可以访问变量color,就是因为可以在作用域链中找到它。

此外,局部作用域中定义的变量可用于在局部上下文中替换全局变量:

var color = "blue";
function changeColor() {
  let anotherColor = "red";
  function swapColor() {
    let tempColor = anotherColor; // red
    anotherColor = color; // blue
    color = tempColor; // red
    console.log(tempColor, anotherColor, color);  // 这里可以访问tempColor, anotherColor, color
  }
  swapColor();  // 这里可以访问anotherColor, color,访问不到tempColor
}
changeColor();  // 这里只能访问color

以上代码,涉及3个上下文:全局上下文、changeColor()的局部上下文和swapColors()的局部上下文。

全局上下文中有一个变量color和一个函数changeColor()。changeColor()的局部上下文中有一个变量anotherColor和一个函数swapColor(),但在这里可以访问全局上下文中的变量color。swapColor()的局部上下文中有一个变量tempColor,只能在这个上下文中访问到。全局上下文和changeColor()的局部上下文都无法访问到tempColor。而在swapColor()中则可以访问另外两个上下文中的变量,因为它们都是父上下文。下图展示这个例子的作用域链

1,JS代码运行的底层机制:

浏览器之所以能给JS代码提供运行环境,是因为浏览器会在计算机内存中分配一块内存,专门用来供代码执行的,这个内存叫栈内存,专业名词叫ECStack(Execution Context Stack)执行环境栈

也就是每打开一个网页,都会形成一个全新的执行环境栈,除了这件事,浏览器默认还会提供很多供JS调用的属性和方法,它把这些属性和方法默认放在GO中,GO是一个堆内存,比如:isNaN、parseInt等

我们之所以能用这些方法,是因为在我们的执行环境栈中或者说浏览器会把所有供我们调用的属性和方法放在一个位置中,这个位置就是全局对象GO(Global Object),

GO的意思:浏览器把内置的一些属性方法放置到单独的内存中,这个内存叫堆内存(Heap),堆内存的目的就是为了存我们用的属性和方法。

任何开辟的内存都有一个16进制内存地址,方便后期找到这个内存

浏览器端会让window指向这个GO,即window代表的就是全局对象。Node的全局对象是global

比如:isNaN方法其实是window.isNaN

栈内存的作用:提供执行代码的环境。

堆内存的作用:存放东西(存放的是属性和方法)

2,执行上下文EC(Execution Context)

执行上下文可以简称上下文

代码执行上下文EC(Execution Context),代码自己执行所在的环境,执行上下文有全局的执行上下文EC(G), 还有函数中的代码都有在一个单独的私有的执行上下文中处理,还有ES6中块级执行的上下文

新开一个网页,我们就会有一个代码执行环境栈内存ECStack,还会形成一个全局的代码执行上下文EC(G),所有的代码最终都要放到栈内存来执行,所以全局执行上下文EC(G)会有一个进栈的过程,这个叫做进栈。执行完代码会出现出栈的过程(全局的不会出栈,全局的在代码不关闭的时候肯定不会出栈),但是我们私有的代码会出栈,出栈即代码释放。出栈是为了防止爆栈,为什么要出栈:

栈内存就是在计算机中分配一块内存,栈内存中形成的执行上下文进到栈中去执行,比如我这个执行上下文进栈执行后不出栈销毁,下一个执行上下文进栈后也不出栈,下下个,....都不销毁的话,

栈内存里的东西越来越多,栈内存的空间就会越来越大,栈内存空间越来越大就会导致我计算机中的内存越来越多,计算机就会越来越卡,最终就会导致整个项目、计算机、页面都会很卡。

栈结构的特点:先进后出

进栈后就是代码自上而下执行,在代码自上而下执行的过程中会做很多事情。当我们执行第一行代码,常规的理解就是创建一个变量/函数,存一个值,那么创建的变量和值存哪呢?

在每一个执行上下文当中都有一个存放当前上下文中所创建的变量和值的地方叫变量对象VO(Varibale Object)

变量对象VO:在当前的上下文当中,用来存放创建的变量和值

每一个执行上下文中都会有一个自己的变量对象,函数(函数私有上下文)中叫AO(Activation Object)活动变量对象,但是也是变量对象,只是VO的一个分支

比如以下基本类型代码是怎么操作的呢?

var a = 12
 var b = a
 b = 13
// 先说var a = 12,共分为3步:1步-创建一个值,2步-创建一个变量,3步-将变量和值关联在一起
// 所有的等号赋值都只是指针的指向(所有的等号赋值都只是指针的关联指向)
// var a = 12, 就是 a 指向 12 建立这样的一个关联
// var b = a 往变量对象里创建一个b, 让 b 和 a 关联,所有的关联都是指针指向
// b = 13 先创建值 13,让 b 重新指向 13, 之前的 值 12 还存在,只是修改了 b 的指针指向,一个变量只能指向一个值,之前的关联就没了
var a = {
  n: 12
}
var b = a
b['n'] = 13
console.log(a.n)   
// 13
// 首先会有一个ECStack执行环境栈,有执行环境栈后,还有个 window = GO,然后我们让全局下的代码执行,就得有个全局执行上下文EC(G)
// 把形成的全局执行上下文EC(G)进栈,代码执行过程中会创建变量,那么就要有一个VO(G)代码变量对象来存储变量,接下来一步一步执行
// var a = {n: 12}  第一步先创建值:这里值是引用类型的值,需要单独进行存储,会单独开辟出一个堆内存,值是对象的时候分为以下3步:
    1,创建一个堆内存
        2,把键值对存储到堆内存中
        3,会把堆内存地址放到栈中,供变量调用
// 第二步:创建一个变量 a
// 第三步:让 a 指向值 {n: 12}所对应的堆内存,比如我们把值{n: 12}所对应的堆内存这个地址称为AF0,此时 a 变量指向的只是{n: 12}这个对象的一个引用地址Af0
// var b = a; 创建变量 b,让它也指向 a 指向的地址
// b['n'] = 13; b基于引用地址找到堆内存,把堆内存中的属性 n 的值进行修改
var obj = {
  name: 'xiaowang',
  fn: (function (n) {
    return n + 10
  })(obj.name) //这行会报错:Cannot read properties of undefined (reading 'name')
}
console.log(obj.fn)

// 第一步:创建值 --- 开辟一个堆,存储键值对,name 和 fn,fn:自执行函数执行 => 先让自执行函数执行,需要将obj.name的值当作实参传递进去,但是现在还没有obj,obj还不存在所以这道题报错,以下代码将不再继续执行下去
// 第二步:创建变量
// 第三步:关联在一起
var a = {},
    b = "0",
    c = 0;
a[b] = "xiaowang";
a[c] = "xiaoli";
console.log(a[b]);  // xiaoli
console.log(a[b]);  // xiaoli
// 代码报错: n is not defined
// 普通对象的属性名只能是字符串(普通对象的属性名可以是基本数据类型值),但是普通对象的属性名不能是对象,如果是对象,会把它转成字符串
// a[n] = 'xiaowang' 相当于 => a['0'] = 'xiaowang'
// a[m] = 'xiaoli' => a[0] = 'xiaoli',这里a[0]数字0也会变成字符串a['0'],到这里可以看出这两句是一样的,所以下面的代码会覆盖上面的代码,所以最后结果是xiaoli
var a = {},
    b = Symbol('1'),
    c = Symbol('1');
a[b] = 'xiaowang'
a[c] = 'xiaoli'
console.log(a[b])  // xiaowang
console.log(a[c])  // xiaoli
var a = {},
    b = {n: '1'},
    c = {m: '2'}
a[b] = 'xiaowang'
a[c] = 'xiaoli'
console.log(a[b]) // xiaoli
console.log(a[c]) // xiaoli
// 普通对象的属性名不能是对象,如果是对象,需要转为字符串存储,普通对象 toString 是调取 Object.prototype.toString 是用来检测诗句类型的
// ({n: '1'}).toString() // "[object Object]"
// ({m: '2'}).toString() // "[object Object]"
2,作用域
Scope(作用域)当前的执行上下文,值和表达式在其中可见或可被访问。如果一个变量或表达式不在当前的作用域中,那么它是不可用的。作用域也可以堆叠成层次结构,子作用域可以访问父作用域,反过来则不行

JS的作用域分为以下三种:

  • 全局作用域:脚本运行代码的默认作用域

  • 模块作用域:模块中运行代码的作用域

  • 函数作用域:由函数创建的作用域

  • 块级作用域:用let和const声明的变量属于块级作用域,用一对花括号(一个代码块)创建出来的作用域。块级作用域只对 letconst 声明有效,对 var 声明无效

由于函数会创建作用域,所以在函数中定义的变量无法从该函数外部访问,也无法从其他函数内部访问

3,作用域链

作用域链是 JavaScript 中用于查找变量的一种机制。它由当前作用域和所有父级作用域的变量对象组成。当访问一个变量时,JavaScript 引擎会首先在当前作用域的变量对象中查找,如果找不到,则会向上一级作用域中查找,直到找到为止,这种向上查找的链路就是作用域链。

作用域链的主要作用是确定变量和函数的可访问性。它保证了在函数内部可以访问外部的变量和函数,但外部无法访问函数内部的变量和函数,从而实现了封装和隔离。

作用域链的形成是在函数定义时确定的,而不是在函数执行时确定的。每当创建一个函数时,都会创建一个新的作用域,并将其添加到作用域链的顶部。当函数执行完毕后,该作用域会被销毁,作用域链也会相应地调整。

4,作用域面试题
(function () {
  var val = 1;
  var json = {
    val: 10,
    dbl: function () {
      // 上级作用域一定是栈不是堆,所以它的作用域是全局
      val *= 2; // => 全局的val = 1 * 2 = 2
      // 如果把上一句代码:val *= 2改为this.val *= 2,这时候的this就是json
    },
  };
  json.dbl();
  alert(json.val + val); // 10 + 2 = 12
})();
// 作用域:栈内存、执行上下文,这仨是一个玩意。var json 等于个对象,首先对象是堆内存,所以json是堆不叫栈,作用域只能是栈

// => "12"
var num = 10;
var obj = { num: 20 };
obj.fn = (function (num) {
  this.num = num * 3;
  num++;
  return function (n) {
    this.num += n;
    num++;
    console.log(num);
  };
})(obj.num);
var fn = obj.fn;
fn(5);
obj.fn(10);
console.log(num, obj.num);
// 22 23 65 30
5,作用域链面试题
var n = 1;
function fn() {
  var n = 2;
  function f() {
    n--;
    console.log(n);
  }
  f();
  return f;
}
var x = fn();
x();
console.log(n);
// 1 0 1

8,undefined、null的区别

1,undefined

Undefined是全局对象的一个属性,也就是说它是全局作用域的一个变量

  • 1,一个声明了却没有被赋值的变量是undefined

  • 2,调用函数的时候,应该提供的参数没提供,该参数也是undefined

  • 3,对象没有赋值的属性,该属性的值也是undefined

  • 4,函数没有返回值的时候,默认值也是undefined

Undefined这个变量从根本上就没有定义,隐藏式空值

2,null

null不是全局对象的属性。null表示空对象,指变量未指向任何对象。null常用在预期的值是一个对象,但是又没有关联的对象的地方使用

Null这个值虽然定义了,但它并未指向任何内存中的对象,声明式空值

3,undefined 和 null的区别

Null和undefined都表示空/没有。主要区别是undefined表示尚未初始化的变量的值,null表示该变量有意缺少对象指向

  • Undefined:意料之外(不是我们能决定的)

    let num; // => 创建一个变量没有赋值,默认值是undefined
    num = 12
    
  • null: 意料之中(一般都是开始不知道值,我们手动先设置为null,后期再给予赋值操作)

let num = null  // => let num = 0;一般最好用null作为初始的空值,因为0不是空值,它在栈内存中有自己的存储空间(占了位置)
num = 12

当检查值是否为null或者undefined的时候,要注意相等(==)和全等(===)运算符的区别,前者会执行类型转换:

typeof null  // "object"
typeof undefined  // "undeinfed"
null == undefined  // true
null === undefined // false
null == null  // true
null === null  // true
!null  // true
Number.isNaN(1 + null)  // false
Number.isNaN(1 + undefined)  // true

9,this

this是函数执行的主体(不是上下文),意思是谁把函数执行的,那么执行主体就是谁

this是谁 和函数在哪创建的或者在哪执行的都没有必然的联系

找this是谁的规律:

1,给元素的某个事件绑定方法,当事件触发方法执行的时候,方法中的this是当前操作的元素本身
document.body.onclick = function (){
  // 此时这个方法中的this是document.body
}
2,方法执行,看方法前面是否有点,有点, 点前面是谁就是谁,没有点,this是window(严格模式下是undefined)。自执行函数中的this是window和undefined
var name = "xiaowang";
function fn() {
  console.log(this.name);
}
var obj = {
  name: "Hello world",
  fn: fn,
};
obj.fn(); // Hello world   => this: obj
fn(); // xiaowang   => this: window(非严格模式下,严格模式下是undefined)    window.fn()把window.省略了


(function () {
 // 自执行函数中的this是window和undefined
})()
3,在构造函数模式执行中,函数体内的this是当前类的实例
function Fn() {
 // this: f这个实例
 this.name = 'xxx'
}
let f = new Fn()
4,箭头函数中没有自己的this,它里面的this是继承函数所处上下文中的this(所以真实项目中,一旦涉及this问题,箭头函数慎用)
window.name = 'WINDOW'
let obj = {
  name: 'OBJ'
}
let fn = n => {
  console.log(this.name)
}
fn(10)  // => this: WINDOW
fn.call(obj, 10) // => this: WINDOW
5,案例
1,ary.__proto__.__proto__.hasOwnProperty()
// hasOwnProperty()方法中的this: ary.__proto__.__proto__   点前面的

2,let obj = {
  fn: (function (n) {
    // 把自执行函数执行的返回结果赋值给fn
    // this: window
  })(10),
};

let obj = {
  fn: (function (n) {
    return function () {
      // => fn 等价于这个返回的小函数
      // this: obj
    };
  })(10),
};
obj.fn();

function fn() {
  console.log(this);   // this: window
}
document.body.onclick = function () {
 // this: document.body
  fn();
};
function Fn() {
  // this: f1
  this.x = 100;
  this.y = 200;
  this.say = function () {
    // this: 这个this是谁,不知道,因为方法此时还没有执行
    console.log(this.x);
  };
}
Fn.prototype.say = function () {
  console.log(this.y);
};
Fn.prototype.eat = function () {
  console.log(this.x + this.y);
};
let f1 = new Fn;
function Fn() {
  // this: f1
  this.x = 100;
  this.y = 200;
  this.say = function () {
    console.log(this.x);
  };
}
Fn.prototype.say = function () {
  console.log(this.y);
};
Fn.prototype.eat = function () {
  console.log(this.x + this.y);
};
Fn.prototype.write = function () {
  this.z = 1000;
};
let f1 = new Fn();
f1.say(); // this: f1 => console.log(this.x) => console.log(f1.x) => 100
f1.eat(); // 私有里面没有eat方法,找原型链上的eat方法,this: f1 => console.log(this.x + this.y) => console.log(f1.x + f1.y) => 300
f1.__proto__.say(); //this: f1.__proto__ => console.log(this.y) => console.log(f1.__proto__.y) => undefined
Fn.prototype.eat(); //this: Fn.prototype => console.log(this.x + this.y) => console.log(Fn.prototype.x + Fn.prototype.y) => undefined + undefined => NaN
f1.write(); //this: f1 => this.z = 1000 => f1.z = 1000 => 给f1设置了一个私有的属性z=1000
Fn.prototype.write(); //this: Fn.prototype => this.z = 1000 => Fn.prototype.z = 1000 => 给原型上设置一个属性z=1000,这个属性是实例的公有属性
/**
 * 面向对象中有关私有/公有方法中的this问题
 * 1,方法执行,看前面是否有点,有点,点前面是谁this就是谁
 * 2,把方法中的this替换
 * 3,再基于原型链查找方式确定结果即可
 * **/
var fullName = "xiaowang";
var obj = {
  fullName: "javascript",
  prop: {
    getFullName: function () {
      return this.fullName;
    },
  },
};
console.log(obj.prop.getFullName()); // 方法前面有点,点前面是谁就是谁 => obj.prop,prop方法里return this.fullName => prop里没有fullName 所以返回undefined
var test = obj.prop.getFullName;
console.log(test()); // test这个方法前面没点,所以是window => window.fullName = > xiaowang
var name = "window";
var Tom = {
  name: "Tom",
  show: function () {
    console.log(this.name);
  },
  wait: function () {
    // this: Tom  所以this.show => Tom.show
    var fun = this.show;
    fun(); // fun()的this => window, 执行show方法 => console.log(this.name) => console.log(window.name) => window
  },
};
Tom.wait();
// => window
window.val = 1;
var json = {
  val: 10,
  dbl: function () {
    this.val *= 2; // => 10 *= 2 = 20
  },
};
json.dbl();
// this: json => json.val *= 2 = 20

var dbl = json.dbl;
dbl();
// this: window => window.val *= 2 => 2

json.dbl.call(window);
// 通过call可以把方法中的this改成window
// this: window (基于call方法改的)  => window.val *= 2 => 2 * 2 => 4

alert(window.val + json.val);
// 4 + 20 = 24
// => "24"

10,hasOwnProperty是什么?

作用:检测某个实例是否在某个对象上

我们学过的in:检测某个属性是否在某个对象上

1,hasOwnProperty和in的区别
console.log(Array.prototype.hasOwnProperty('push')) // true
console.log([] in Array) // false
2,怎么判断是不是自己的私有属性
// 检测某个属性是否为对象的公有属性 hasPubProperty
Object.prototype.hasPubProperty = function (property) {
  if (!["string", "number", "boolean"].includes(typeof property))
    return false;
  return property in this && !this.hasOwnProperty(property);
};
console.log(Array.prototype.hasPubProperty("hasOwnProperty")); // => true
console.log(Array.prototype.hasPubProperty("push")); // => false

11,科普什么是进程、内存、CPU、线程、JS的执行机制、同步、异步、事件循环

1,进程

电脑每打开一个程序就是一个进程,或者浏览器中每打开一个页面也都是一个进程

任何一个程序,无论是用何种语言写的代码,当它在运行的时候,比如打开 QQ,在管理器会发现有一个进程在运行着,这就是我们经常说的一段程序执行的过程,每一个运行的程序都有一个进程,这些进程支撑着软件应用提供给我们的各种功能

浏览器的工作方式

前端代码主要的运行环境就是浏览器,具体说就是浏览器内部的JS引擎(V8引擎)及渲染引擎。另一个是Node服务端开发的环境,其实还是从Chrome浏览器中抽取的V8引擎。

浏览器的请求是并发进行的,因为本质上它是给我们提供服务的,当页面加载的时候,需要的资源都是从服务端过来的一条条的请求,比如页面打开的时候有5个接口需要返回数据,还有图片、CSS资源、音视频资源等,假设这些合起来需要请求20个请求,实际上浏览器在处理这些请求的时候并不是一次性将20个请求一起发过去,而是有请求并发限制的,减少处理的时候浏览器本身的线程切换开销。比如Chrome,它实际上是6条并发进行,某一条请求完成后补充另外的请求进来,直到20条请求完毕。

浏览器的请求并发限制针对的是同一个域名下的资源,这样的话,我们就可以将静态资源和服务分离,分多域名存储,这样就可以简单的解决浏览器的并发瓶颈

比如以下小案例,分析一下浏览器的请求并发数:

<body>
    <img src="./1.jpg" alt="" />
    <img src="./2.jpg" alt="" />
    <img src="./3.jpg" alt="" />
    <img src="./4.jpg" alt="" />
    <img src="./5.jpg" alt="" />
    <img src="./6.jpg" alt="" />
    <img src="./7.jpg" alt="" />
    <img src="./8.jpg" alt="" />
    <img src="./9.jpg" alt="" />
    <img src="./10.jpg" alt="" />
    <img src="./11.jpg" alt="" />
    <img src="./12.jpg" alt="" />
    <img src="./13.jpg" alt="" />
    <img src="./14.jpg" alt="" />
    <img src="./15.jpg" alt="" />
    <img src="./16.jpg" alt="" />
    <img src="./17.jpg" alt="" />
    <img src="./18.jpg" alt="" />
    <img src="./19.jpg" alt="" />
    <img src="./20.jpg" alt="" />
  </body>

以上代码罗列了20张图片资源,打开Network面板,选择All请求类型,可以看到如下界面:

图中可以清晰的看出并发数限制。这其实就像赛道接力,同时奔跑在赛道上的永远不超过跑道数(比如6),只有某一个赛道某一段跑完了,这个赛道才能释放出来,接着由下一个人跑。

并发

并发是指某一个瞬间运行在同一处理机或者某个服务器集群上的服务或执行逻辑。比如,淘宝双十一当天0点刚过的那一瞬间,用户的每一次点击、支付都是一条服务,需要服务端去处理,可以想象一下那时候的人数和同时过来的请求数,这就是并发。

2,内存

内存就是所谓的栈内存和堆内存

3,CPU

CPU就是处理器

4,线程

线程:一个进程里可以同时处理很多任务,每一个任务都是基于线程处理的这就是线程

比如一个QQ进程,既能拍照又能聊天还可以语音、听音乐等,在一个QQ里可以同时做诸如以上的很多事情,这就是线程

进程在开启的时候,系统会给它分配一点资源,如内存、CPU计算资源、网络带宽、磁盘读写等,而线程就是在进程的基础上,从进程那里再分配一点资源来运行。线程是进程的一个实体,它只占用了一点运行时的资源,但它同样共享着其所属进程所拥有的全部资源。相比于进程,线程可以更高效地利用 CPU 资源

例如,A与B在QQ上聊天,C突然来消息了,此时的A同时与B和C聊天,用户只需要进行窗口切换,然而程序需要处理的逻辑是不同的用户关联不同的信息。每个程序只有一个进程,要同时完成两件事,一个很简单的办法就是再开一个进程,实现真正意义上的同时执行。

进程与进程之间的切换是很耗时间的,这时候线程就来了,进程能做的事情线程都能做,而且只需要很少的运行资源。线程之间的切换也比进程快得多,大大地提高了系统的性能。

其实,开的线程实际上还是一个进程,那它是怎样做到接近并行的呢?实际上,线程是把 CPU 的工作时间分成几个片段,一个片段执行一个任务。由于它们之间切换效率极高,在用户看来就像是在同时工作,这类似于我们看电视、计算机等的情况。实际上电视和计算机的屏幕是以一种很快的频率在刷新,看起来就像是连贯的,这个过程利用的就是 CPU 的调度算法。

5,JS 的执行机制

前面我们知道了大多数程序在并发完成某个任务的时候,实际上是开了一条线程在跑。以php举例,当有一个任务或者请求时,可以从线程池里取一条线程运行,当线程处理完请求或操作逻辑后重新放回线程池中。

这样的方式是现在大多数后台语言的处理方式,由于线程有自己独立的堆栈,会比较安全。这种方式也存在一个问题:线程并不能无限的新增,这个线程池里的线程实际上是有限的,当并发量非常高的时候,就会发现线程不够用。当线程不够用的时候,剩余的任务只能等线程释放出来再执行。尽管线程的运行速度非常快,切换速率也高,但是也抵挡不住庞大的并发量,因此通过优化可以极大的提高运行性能。

那么JS的运行机制是怎样的呢?

JS 实际上只有一个线程,它是怎样同时做不同的事情的呢?以Node为例,JS的处理可以看作是一位分配任务的领导,当收到一个任务或者请求的时候,JS引擎将该任务放入任务队列中,JS的同步主线程仅仅做了一个标识,用来接受及分配任务或请求。

这个过程不需要做任何的处理,类似于倾听和记录,这样做就可以非常高效的处理任务了。在该任务中,任务A在某个时间节点开始执行, 任务B在另一个时间节点开始执行,任务与任务之间不会互相等待、阻塞,任务其实几乎是同时进行的,这样大大的加快了程序的执行效率。JS虽然是单线程的,但是通过这种异步队列的方式分配和执行任务,可以大大的提高程序的执行性能。

任务队列和线程最大的不同是队列只要内存足够,可以一直堆叠任务。这样会造成很大的内存开销,但其实如金硬件成本越来越低,增加一点内存就能极大的提高用户体验

6,同步

同步:任务是依次执行的,上一个任务没有完成,下一个任务不能进行处理,即一个一个执行。它是基于浏览器的单线程完成的

7,异步

异步:上一个事情没有完成,下一个事情也可以继续完成,它是基于浏览器的多线程完成的

JS也是单线程的(大部分任务都是同步任务),但是也有异步任务:

  • 1,定时器(设置定时器是同步的,多长时间之后那个方法这件事是异步的)

  • 2,事件绑定

  • 3,ajax数据请求项目中基本都是用异步处理

  • 4,回调函数可以理解为异步

  • 5,Promise/async/await就是用来处理异步编程的

  • 6,nodejs中也提供其余的异步编程方式

8,事件循环

因为浏览器是多线程的, 浏览器只分配一个线程用来自上而下执行我们的代码,所以JS中大部分任务都属于同步任务,但是肯定有异步任务,比如定时器...等,那么浏览器是怎么处理JS中的异步任务的呢,首先主线程在自上而下执行的时候,代码进栈,进栈后出栈,反反复复,当遇到异步任务的时候,会把当前这个异步任务放到等待任务队列(Event Queue)中存起来,主线程的代码继续加载执行,当把主线程所有代码都加载执行完了,也就是主线程空闲下来的时候,就到等待任务队列中查找到达时间的任务,拿到主线程所在内存中执行,当执行完,再去等待任务队列看看还有哪一个到时间了再拿到主线程来执行,反反复复,这么个过程就是事件循环

定时器设置一个等待时间,到达时间后不一定执行(如果当前主线程被占用着,所有任务都要等主线程空闲下来,才能被安排执行),因为JS是单线程的,一次只能干一件事:

let n = 0;
setTimeout(() => {
  n++;
  console.log(n);
}, 0);
console.time("AA");
for (let i = 0; i < 900000000; i++) {}
console.timeEnd("AA");
n += 2;
console.log(n);

以上代码输出结果为: AA: 741.64208984375 ms 2 3

主线程被死循环牵绊住,不会再执行Event Queue里的代码:

let n = 0;
setTimeout(() => {
  n++;
  console.log(n); // => 没有执行,因为主线程被死循环牵绊住了
}, 0);
n += 2;
console.log(n); // => 2
while (1 === 1) {}

Event Queue中,谁先到时间先执行谁:

let n = 0;
setTimeout(() => {
  n++;
  console.log(n);
}, 500);
setTimeout(() => {
  n += 2;
  console.log(n);
}, 50);
for (let i = 0; i < 90000000; i++) {}
setTimeout(() => {
  n += 3;
  console.log(n);
}, 20);
console.log(n);

12,事件及浏览器常用的事件行为

1,事件是什么

事件是元素天生自带的默认行为,无论我们是否给其绑定了方法,当我们操作的时候,也会把对应的事件触发

2,事件绑定是什么

事件绑定是给元素的某个行为绑定一个方法,目的是当前行为触发的时候,可以做一些事情

3,常见的事件行为
1,鼠标事件
click 点击 (移动端click被识别为单击)
dbclick 双击
mousedown 鼠标按下
mouseup 鼠标抬起
mousemove 鼠标移动
mouseover 鼠标滑过
mouseout 鼠标滑出
mouseenter 鼠标进入
mouseleave 鼠标离开
mousewhell 鼠标滚轮滚动
2,键盘事件
keydown 按下某个键
keyup 抬起某个键
keypress 除shift/fn/capsLock键外,其他键按住(连续触发)
3,移动端手指事件
touchstart 手指按下
touchmove 手指移动
touchend 手指松开
touchcancel 操作取消(一般应用在非正常状态下操作结束)
4,表单元素常用事件
focus  获取焦点
blur 失去焦点
change 内容改变
5,音视频常用事件
canplay 可以播放(资源没有加载完,播放中可能会卡顿)
canplaythrough 可以播放(资源已经加载完,播放中不会卡顿)
play 开始播放
playing 播放中
pause 暂停播放
6,其他常用事件
load 加载完
unload 资源卸载
beforeunload 当前页面关闭之前
error 资源加载失败
scroll 滚动事件
readystatechange ajax请求状态改变事件
contextmenu 鼠标右键触发
4,DOM0和DOM2事件绑定的区别
  • DOM0:

    // 元素.on事件行为 = function(){}
    eg: box.onclick = function () {}
    
  • DOM2:

    // 元素.addEventListner(事件行为, function(){}, true/false)
    eg: box.addEventListner('click', function(){}, true)
    

DOM0事件绑定的原理:给元素的私有属性赋值,当事件触发,浏览器会帮我们把赋的值执行,但是这样也导致“只能给当前元素某一个事件行为绑定一个方法:

let lufei = document.querySelector(".lufei");
lufei.onclick = function () {
  console.log("哈哈哈");
};
lufei.onclick = function () {
  console.log("呵呵呵");
};
// => 只执行最后一个,输出呵呵呵

DOM2事件绑定的原理:基于原型链查找机制,找到EventTarget.prototype上的方法并且执行,此方法执行,会把给当前元素某个事件行为绑定的所有方法,存放到浏览器默认的事件池中(绑定几个方法,会向事件池存储几个),当事件行为触发,会把事件池中存储的对应方法一次按照顺序执行“给当前元素某一个事件行为绑定多个不同方法:

let lufei = document.querySelector(".lufei");
lufei.addEventListener(
  "click",
  function () {
    console.log("哈哈哈");
  },
  false
);
lufei.addEventListener(
  "click",
  function () {
    console.log("呵呵呵");
  },
  false
);
// => 两个都会执行输出哈哈哈 呵呵呵

DOM2事件绑定的时候,我们一般采用实名函数,目的:这样可以基于实名函数去移除事件绑定

function fn () {
  console.log('hahaha')
  // => 移除事件绑定:从事件池中移除,所以需要指定好事件类型、方法等信息
  lufei.removeEventListener('click', fn, false)
}
lufei.addEventListener('click', fn, false)

5,事件对象

给元素的事件行为绑定方法,当事件行为触发方法会被执行,不仅被执行,而且还会把当前操作的相关信息传递给这个函数 => 事件对象

如果是鼠标操作,获取的是 mouseEvent 类的实例 =》 鼠标事件对象

鼠标事件对象:mouseEvent.prototype -> UIEvent.prototype -> Event.prototype -> Object.prototype

如果是键盘操作,获取的是keyboardEvent类的实例 => 键盘事件对象

6,阻止事件的默认行为

阻止a标签的默认行为

// => 方式1
<a href="javascript:;">11111</a>  // => 给href加javascript:;

// => 方式2: 点击a标签,先触发click行为,然后再去执行href的跳转
<a href="http://www.baidu.com" id="link">11111</a>
link.onclick = function () {
  return false; // => 返回一个false,相当于结束后面即将执行的步骤
};

// => 方式3:点击a标签,先触发click行为,然后再去执行href的跳转
<a href="http://www.baidu.com" id="link">11111</a>
link.onclick = function (ev) {
  ev.preventDefault();
};
7,事件的传播机制

冒泡传播:触发当前元素的某一个事件行为,不仅它的这个行为被触发了,而且它所有的祖先元素(一直到window)相关的事件行为都会被依次触发(从内到外的顺序)

阻止冒泡传播:

box.onclick = function(ev){
 ev.stopPropagation()  // => 阻止冒泡传播
}

事件的传播机制:

捕获阶段:从最外层向最里层事件源依次进行查找(目的:是为冒泡阶段事先计算好传播的层级路径)

目标阶段:当前元素的相关事件行为触发

冒泡传播:触发当前元素的某一个事件行为,不仅它的这个行为被触发了,而且它所有的祖先元素(一直到window)相关的事件行为都会被依次触发(从内到外的顺序)

8,mouseover 和mouseenter的本质区别

13,浏览器常用的事件-事件委托

1,事件委托
1,事件冒泡

事件开始时,由最具体的元素逐级向上传播,直到较为不具体的节点(文档)

2,事件捕获

不太具体的节点更早的接收到事件,最具体的节点最后接收到事件

3,事件流

事件流包括三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段

首先发生的是事件捕获阶段,为截获事件提供机会。然后是实际的目标接收到事件,最后一个阶段是冒泡阶段,可以在这个阶段对事件做出响应

4,所谓的事件委托是什么?

是基于事件的冒泡传播机制完成的

5,什么时候用事件委托,用它的好处?

如果一个容器中很多元素都要在触发某一事件的时候做一些事情,比如有100个元素:给每一个元素都单独进行事件绑定。

使用事件委托:我们只需要给当前容器的这个事件行为绑定方法,这样不论是触发后代中的哪一个元素的相关事件行为,由于冒泡传播机制,当前容器绑定的方法也都要被触发执行

6,事件对象的ev.target事件源

想知道点击的是谁,只需要基于事件对象中的ev.target事件源获取即可

2,事件委托的意义
1,基于事件委托实现,整体性能要比一个个的绑定方法高出50%左右
2,如果多元素触发,业务逻辑属于一体的,基于事件委托来处理,更加的好
3,某些业务场景只能基于事件委托来处理
3,案例
1,购物车案例
// 方法一:
<div class="box">
  <span>购物车</span>
  <div class="detail">暂无购物车内容~</div>
</div>
.box {
  width: 100px;
  height: 35px;
  line-height: 35px;
  border: 1px solid #AAA;
  text-align: center;
  margin: 20px auto;
  box-sizing: border-box;
  position: relative;
  color: #AAA;
}
.detail {
  width: 200px;
  height: 100px;
  position: absolute;
  top: 33px;
  right: -1px;
  box-sizing: border-box;
  border: 1px solid #AAA;
  text-align: left;
  color: #AAA;
  display: none;
}
.box:hover .detail {
  display: block;
}
// 方法二:
<div class="container">
  <div class="box">
    <span>购物车</span>
  </div>
  <div class="detail">暂无购物车内容~</div>
</div>
.container {
  box-sizing: border-box;
  width: 200px;
  margin: 20px auto;
}
.box {
  width: 100px;
  height: 35px;
  line-height: 35px;
  float: right;
  border: 1px solid #AAA;
  text-align: center;
  box-sizing: border-box;
  color: #AAA;
  position: relative;
  top: 1px;
}
.detail {
  width: 200px;
  height: 100px;
  float: right;
  box-sizing: border-box;
  border: 1px solid #AAA;
  text-align: left;
  color: #AAA;
  display: none;
}
let box = document.querySelector('.box'),
detail = document.querySelector('.detail')
document.onmouseover = function (ev) {
  console.log(ev.target)
  let target = ev.target
  // if (target.tagName === 'span') {
    // 如果事件源是span, 我们让其变成它的父元素
  //  target = target.parentNode
  // }
  target.tagName === 'span' ? target = target.parentNode : null;
  if (/^(box|detail)$/.test(target.className)) {
    // target.className === 'box' || target.className === 'detail'
    // 如果事件源是box和detail,让其显示
    detail.style.display = 'block'
    return
  }
  detail.style.display = 'none'
}
2,追加按钮点击并弹出相应的li的索引案例
// 方法1:
let $ulList = $('.ulList'),
  $btn = $('.btn'),
  $list = null
function handle () {
  $list = $ulList.children('li')
  $list.each(function (index, item) {
    $(item).click(function () {
      alert(`我是第 ${index + 1} 个li`)
    })
  })
}


handle()
let count = 5
$btn.click(function () {
  let str = ``
  for (var i = 0; i < 5; i++) {
    count++
    str += `<li>我是第 ${count} 个li</li>`
  }
  $ulList.append(str)
  handle()
})
// 方案2: 改为事件委托方式
let $ulList = $('.ulList'),
  $btn = $('.btn'),
  count = 5

$ulList.click(function (ev) {
  let target = ev.target;
  $target = $(target)
  if (target.tagName === 'LI') {
    alert(`我是第 ${$(target).index() + 1} 个li`)
  }
})

$btn.click(function () {
  let str = ``
  for (var i = 0; i < 5; i++) {
    count++
    str += `<li>我是第 ${count} 个li</li>`
  }
  $ulList.append(str)
})

14,var、let、const的区别

【声明】:该题参考阮一峰大佬的ECMAScript 6 入门和自我的总结归纳,部分觉得阮老写的很好,没有修改的必要,即使修改也改不出花来,所以直接拿过来了

1,区别总结
  • 1,块级作用域方面
    • var 定义的变量没有块的概念,可以跨块访问,不能跨函数访问

    • let 定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问

    • const 定义的变量,只能在块作用域里访问

  • 2,是否存在变量提升
    • var 命令会发生变量提升现象,即变量可以在声明之前使用,值为undefined

    • let和 const 命令不会发生变量提升

  • 3,暂时性死区
    • 暂时性死区:在代码块内,使用letconst声明变量之前,该变量是不可用的

  • 4,是否可以被重复声明
    • let、const 命令在相同作用域中, 不能重读声明

  • 5,const
    • const声明一个只读的常量,一旦声明,常量的值不能改变

    • 声明的基本数据类型不能被修改,声明的引用数据类型可以通过修改对象属性和方法,本质上,基本数据类型的数据值爆存在变量指向的那个内存地址,因此等同于常量。但是引用数据类型的数据,变量指向的内存地址保存的只是一个指向实际数据的指针,const只能保证这个指针式固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了

2,区别详细介绍
1,块级作用域

ES5中的作用域有:全局作用域函数作用域,没有块级作用域的概念

ES6新增了块级作用域,由{}包括,if语句和for语句里面的{}都属于块级作用域

// 块级作用域
{
  var a = 100;
  let b = 200;
  const c = 300;
  console.log(a); // 100
  console.log(b); // 200
  console.log(c); // 300
}
console.log(a); // 100
console.log(b); // b is not defined
console.log(c); // c is not defined

// 函数作用域
(function () {
  var a = 100;
  let b = 200;
  const c = 300;
  console.log(a);
  console.log(b);
  console.log(c);
})();
console.log(a);
console.log(b);
console.log(c);
2,变量提升
console.log(a); // undefined
var a = 1;

console.log(b); // 报错 ReferenceError
let b = 2;

console.log(c); // 报错 ReferenceError
const c = 3;
3,暂时性死区

在代码块内,使用letconst声明变量之前,该变量是不可用的。这在语法上成为暂时性死区简称TDZ

if (true) {
  // TDZ开始
  a = 10;
  console.log(a); // 报错 ReferenceError

  let a; // TDZ结束
  console.log(a); // undefined

  a = 20;
  console.log(a); // 20
}

ES6 规定暂时性死区和letconst语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了

let能解决typeof检测时出现的暂时性死区问题,所以let比var更严谨

console.log(a)  // a is not defined
console.log(typeof a)  // "undefined" 这是浏览器的bug,本应该报错的,因为没有a(暂时性死区)

console.log(typeof a); // Cannot access 'a' before initialization
let a;
4,不允许重复声明
function f() {
  let a = 10;
  var a = 1;
} // 报错

function f() {
  const a = 10;
  let a = 1;
}  // 报错
5,const声明的变量可以被修改吗
// 1,const声明一个只读的常量,一旦声明,常量的值不能改变
const a = 100
a // 100
a = 200  // TypeError

// 是否可被修改
// 基本数据类型
const a = 100;
a = 200;
console.log(a); // TypeError

// 引用数据类型
const o = {
  name: "xiaowang",
  age: 18,
};
o = {
  name: "Tom",
  age: 19,
};
console.log(o);  // TypeError

const o = {
  name: "xiaowang",
  age: 18,
};
o.age = 19;
console.log(o); // {name: 'xiaowang', age: 19}
3,经典面试题
for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  }, 1000);
}
console.log(i);

setTimeout是异步执行的,1000ms后向任务队列里添加一个任务,只有主线程的全部执行完才会执行任务队列里的任务,所以当主线程 for 循环执行完之后 i 的值为5 ,这时候再去执行任务队列里的任务i就变成6了。

每次for循环的时候,setTimeout都会执行,但是里面的function则不会执行被放入任务队列,所以放了6次,for循环的6次执行完后不到1000ms,1000ms后全部执行任务队列里的函数,所以输出6个6

// 将以上代码中的var改为let
for (let i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  }, 1000);
}
console.log(i);

let 定义的 i 是块级作用域,每个 i 只能存活到大括号结束,并不会把后面的for循环的i值赋给前面的setTimeout中的i,而var定义的i是局部变量,这个i的生命周期不受for循环的大括号限制

15,JS文件放在head里还是body里,什么区别?

答案:只是单纯的Script标签的情况下,JS文件 应该放在 body 元素中的页面内容后面,如果Script标签中添加deferasync属性,则不考虑此问题

1,JS文件 在 body 元素中

JS文件 在 body 元素中的引入位置如下:

<! DOCTYPE html>
<html>
  <head>
  <title>Example HTML Page</title>
  </head>
  <body>
  <! -- 页面内容 -->
  <scriptsrc="example1.js"></script>
  <scriptsrc="example2.js"></script>
  </body>
</html>

如果把JS文件放在head元素里,这意味着必须把所有JS文件都下载、解析完后才能开始渲染页面,对于需要引入很多JS的页面,就会导致页面渲染明显延迟,在此期间浏览器窗口完全空白。为了解决这个问题,就把JS引入放在body元素中的页面内容后面,这样以来,页面会在处理JS代码之前安全渲染页面,浏览器空白页面的时间就会减短。

2,async、defer

之前分享过的文章里有defer、async的详细介绍 - 传送门地址2024前端高频面试题之HTML&CSS篇的第6题,这里我就简单提下就行了。

async、defer都是异步加载 js,不同的是 async 是 js 一加载完就会马上执行,不管 html 有没有解析完毕,所以它有可能阻塞 html 解析。而 defer 要等到 html 解析完毕之后才执行,所以不会阻塞 html 解析

【写在结束】:如果文章对你有所帮助,希望可以帮我点点赞和关注哦,谢谢~,也可以关注我的公众号:【前端常见面试题】期待你的关注