Web前端基础知识:JS篇

454 阅读41分钟

先导

个人在学习过程中,涉及和遇见的一些基础知识,对其进行了简单归纳总结,浅尝辄止,略显杂而不精,做个人参考用

注:内容基本都是摘抄自博客、网络或MDN等

JS篇

1、JS为什么是单线程

这主要和js的用途有关,js是作为浏览器的脚本语言,主要是实现用户与浏览器的交互,以及操作dom;这决定了它只能是单线程,否则会带来很复杂的同步问题。 举个例子:如果js被设计了多线程,如果有一个线程要修改一个dom元素,另一个线程要删除这个dom元素,此时浏览器就会一脸茫然,不知所措。所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质


2、变量提升、函数提升

参考链接

变量提升: 变量声明的提升是以变量所处的第一层词法作用域为“单位”的,即全局作用域中声明的变量会提升至全局最顶层,函数内声明的变量只会提升至该函数作用域最顶层。

函数提升: 只会提升函数声明(函数提升是可以直接在函数声明之前调用该函数,并能成功执行它),而不会提升函数表达式(函数表达式就可以看作成变量提升)

进入执行上下文时,首先会处理函数声明,其次会处理变量声明

解释:

  • js之前只有全局作用域和函数作用域
  • 函数作用域中的变量声明会屏蔽全局作用域
  • 函数作用域中会再进行变量、函数提升

我们可以在某个变量或者函数定义之前访问这些变量,这即是所谓的变量提升(Hoisting)。传统的 var 关键字声明的变量会被提升到作用域头部,并被赋值为 undefined

变量提升只对 var 命令声明的变量有效,如果一个变量不是用 var 命令声明的,就不会发生变量提升。

:ES6 引入了块级作用域,有认为块级作用域中使用 let 声明的变量同样会被提升,只不过不允许在实际声明语句前使用

基础的函数提升同样会将声明提升至作用域头部,不过不同于变量提升,函数同样会将其函数体定义提升至头部

注意

  • JavaScript中的函数是一等公民,函数声明的优先级最高,会被提升至当前作用域最顶端 即函数的变量提升会在变量之前,函数表达式等同与变量,同名的话也会覆盖前面的

  • js的同名函数(不管参数)只会用最后一个,即同一个作用域中存在多个同名函数声明,后面出现的将会覆盖前面的函数声明

  • 函数提升不会被同名变量声明时覆盖,但是会被同名变量赋值后覆盖,即只声明不赋值的话,不会覆盖函数


3、 闭包

什么是闭包?

MDN

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

技术实现

在某个内部函数的执行上下文创建时,会将父级函数的活动对象加到内部函数的 [[scope]] 中,形成作用域链,所以即使父级函数的执行上下文销毁(即执行上下文栈弹出父级函数的执行上下文),但是因为其活动对象还是实际存储在内存中可被内部函数访问到的,从而实现了闭包。

为什么父级函数上下文已经被销毁还可以访问其域内的变量?

因为js维护了一个作用域链


4、执行上下文与词法环境

1)执行上下文

(1)执行上下文栈

js可执行代码:全局代码、函数代码、eval代码。

当执行到一个函数的时候,就会进行准备工作,这里的“准备工作”,就叫做"执行上下文(execution context),管理上下文就使用栈方式,初始化会向栈内压入一个全局执行上下文,当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出

(2)执行上下文三个重要属性

参考链接:JavaScript深入理解

  • 变量对象

    变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。

  • 作用域链

    当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

  • this

    this的设计目的就是在函数体内部,指代函数当前的运行环境(Context) 阮一峰:this原理

细节:

  • 创建:函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中

  • 激活:当函数激活时,进入函数上下文,创建 VO/AO 后,就会将活动对象添加到作用链的前端

  • 流程

    1. 函数创建,保存作用域链到属性[[scope]],注意这个时候只有父级的对象

    2. 函数执行,上下文入栈

    3. 函数激活,创建活动对象,而活动对象包括两部分:arguments对象和Scope作用域链,于是复制上下文中的[[scope]],同时用arguments创建,然后初始化活动对象,加入 形参、函数声明、变量声明。最后把活动对象添加到作用域链的前端,准备工作完成

    4. 函数运行,函数体运行操作,修改活动对象的属性值,按代码返回指定属性

    5. 函数结束,上下文从栈内弹出

2)词法环境

参考链接:执行上下文和词法环境

在 JavaScript 中,每个运行的函数,代码块 {...} 以及整个脚本,都有一个被称为 词法环境(Lexical Environment) 的内部(隐藏)的关联对象。

词法环境对象由两部分组成:

  • 环境记录(Environment Record) —— 一个存储所有局部变量作为其属性(包括一些其他信息,例如 this 的值)的对象。
  • 外部词法环境 的引用,与外部代码相关联。

简单说,就是一内一外,变量 就是(特殊的内部对象,即词法环境记录对象)对象的属性

函数调用时会自动创建一个新的词法环境,那这样就有了外部的词法环境即全局上下文, 内部词法环境会引用外部

当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。

技术实现: 所有函数都有名为 [[Environment]] 的隐藏属性,该属性保存了对创建该函数的词法环境的引用

在变量所在的词法环境中更新变量


5、script 标签中 defer 和 async 的区别?

1)分析

script标签分析

在正常情况下,即 <script> 没有任何额外属性标记的情况下,有几点共识

  • JS 的脚本分为加载解析执行几个步骤,简单对应到图中就是 fetch (加载) 和 execution (解析并执行)
  • JS 的脚本加载(fetch)且执行(execution)会阻塞 DOM的渲染,因此 JS 一般放到最后头

2)区别

script :会阻碍 HTML 解析,只有下载好并执行完脚本才会继续解析 HTML。

async script :解析 HTML 过程中进行脚本的异步加载,加载完成后立即执行,因此可能会阻塞DOM解析

defer script:异步加载,不会阻碍 HTML 的解析,执行放到DOM解析完成之后,按顺序执行,但会在事件 DomContentLoaded 之前

引申:DomContentLoaded事件在Load之前

  • 初始的 HTML 文档被完全加载和解析完成之后,DomContentLoaded 事件被触发,而无需等待样式表、图像和子框架的完全加载.
  • 当整个页面及所有依赖资源如样式表和图片都已完成加载时,触发load事件

6、Event Loop

参考链接: Event Loop

(1) 同步任务和异步任务

Javascript单线程任务被分为同步任务异步任务

同步任务会在调用栈中按照顺序等待主线程依次执行,

异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行

(2) 宏任务和微任务(应该属于异步任务)

宏任务:

script全部代码、setTimeoutsetIntervalsetImmediate(浏览器暂时不支持,只有IE10支持,具体可见MDN)、I/OUI Rendering

微任务:

Process.nextTick(Node独有)PromiseObject.observe(废弃)MutationObserver

注:process.nextTick 永远大于 promise.then, 原因:在Node中,_tickCallback在每一次执行完TaskQueue中的一个任务后被调用,而这个_tickCallback中实质上干了两件事:

  1. nextTickQueue中所有任务执行掉(长度最大1e4,Node版本v6.9.1)
  2. 第一步执行完后执行_runMicrotasks函数,执行microtask中的部分(promise.then注册的回调) 所以很明显 process.nextTick > promise.then

个人理解为process.nextTick推入到了当前宏任务的最后,类似于defer

或者说nodejs中有一个额外的队列,nextTickQueue

(3) 流程

执行栈在执行完同步任务后,查看执行栈是否为空,如果执行栈为空,就会去执行Task(宏任务),每次宏任务执行完毕后,检查微任务(microTask)队列是否为空,如果不为空的话,会按照先入先出的规则全部执行完微任务(microTask)后,设置微任务(microTask)队列为null,然后再执行宏任务,如此循环。

注意:

  • 宏任务队列和微任务队列是两条不同的队列,在当前loop中,先执行已经在当前栈中的宏任务,执行完时会去查看并运行微任务队列,直到微任务队列为空时,再去宏任务队列取出队首的任务,放入执行栈,执行,执行完毕后调用栈为空

  • run script没有明确说明是宏任务,但认为是在执行顺序上好像也没有影响

  • 如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行;


7、JS的继承

JavaScript实现继承共6种方式:

  • 原型链继承

    这种实现方式存在的缺点是,在包含有引用类型的数据时,会被所有的实例对象所共享,容易造成修改的混乱。还有就是在创建子类型的时候不能向超类型传递参数。

  • 借用构造函数继承

    通过在子类型的函数中调用超类型的构造函数来实现的,这一种方法解决了不能向超类型传递参数的缺点,但是它存在的一个问题就是无法实现函数方法的复用,并且超类型原型定义的方法子类型也没有办法访问到。

  • 组合继承

    原型链和借用构造函数组合起来使用的一种方式。通过借用构造函数的方式来实现类型的属性的继承通过将子类型的原型设置为超类型的实例来实现方法的继承。这种方式解决了上面的两种模式单独使用时的问题,但是由于我们是以超类型的实例来作为子类型的原型,所以调用了两次超类的构造函数,造成了子类型的原型中多了很多不必要的属性。

  • 原型式继承

    基于已有的对象来创建新的对象,实现的原理是,向函数中传入一个对象,然后返回一个以这个对象为原型的对象。这种继承的思路主要不是为了实现创造一种新的类型,只是对某个对象实现一种简单继承,ES5 中定义的 Object.create() 方法就是原型式继承的实现。缺点与原型链方式相同。

  • 寄生式继承

    创建一个用于封装继承过程的函数通过传入一个对象,然后复制一个对象的副本,然后对象进行扩展,最后返回这个对象。这个扩展的过程就可以理解是一种继承。这种继承的优点就是对一个简单对象实现继承,如果这个对象不是我们的自定义类型时。缺点是没有办法实现函数的复用。

  • 寄生组合式继承

    组合继承的缺点就是使用超类型的实例做为子类型的原型,导致添加了不必要的原型属性。寄生式组合继承的方式是使用超类型的原型的副本来作为子类型的原型,这样就避免了创建不必要的属性。

一图了解js继承


8、JS原型链

1)__proto__prototype

参考阮一峰的:Javascript继承机制的设计思想

了解起源

(1)prototype

简单说就是:

javascript设计初的思想是一切都是对象,那怎么创建对象呢,就引入了new和构造函数,但是如果光使用new 加构造函数,每一个生成的对象实例就相当于单独有了一份拷贝,无法数据共享,所以为了数据共享和节省资源,

设计者为构造函数设置一个prototype属性,这个属性包含一个对象(即prototype对象,原型对象),

所有实例对象需要共享的属性和方法,都放在这个对象里面;那些不需要共享的属性和方法,就放在构造函数里面。 这样实例对象一旦创建,将自动引用prototype对象的属性和方法。也就是说,实例对象的属性和方法,分成两种,一种是本地的(constructor,构造函数中声明的),另一种是引用的(prototype,原型对象里面的)。

于是得到:prototype是函数的属性,且prototype属性是一个对象,就是常说的原型/原型对象

形如:

{
    constructor: ƒ doSomething(),
    __proto__: {
        constructor: ƒ Object(),
        hasOwnProperty: ƒ hasOwnProperty(),
        isPrototypeOf: ƒ isPrototypeOf(),
        propertyIsEnumerable: ƒ propertyIsEnumerable(),
        toLocaleString: ƒ toLocaleString(),
        toString: ƒ toString(),
        valueOf: ƒ valueOf()
    }
}

(2)__proto__

每一个js对象有一个__proto_ 属性,它指向该对象的原型。

__proto__是一种非规范属性,Web标准已经删除它,但在ES6中它是规范标准化的,用于确保Web浏览器的兼容性,它已被不推荐使用, 现在更推荐使用

Object.getPrototypeOf/Reflect.getPrototypeOf

Object.setPrototypeOf/Reflect.setPrototypeOf。

个人理解:

函数有其原型对象,那么为了溯源和方便使用查找,根据函数生成的对象,应该是可以查找到是由哪个构造函数生成,并且是不是有哪些不属于本身但属于原型的属性方法存在,可以使用,那就必须知道它的构造函数的原型对象,于是就有了__proto__属性

(3)constructor

每个原型都有一个constructor属性,constructor属性指向关联的构造函数,而原型的constructor指向构造函数自身

基于以上,我们大部分情况就可以把__proto__理解为构造函数的原型对象,

__proto__===constructor.prototype

例外: 通过 Object.create()创建的对象有可能不是, Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

(3)总结

__proto__constructor属性是对象所独有的;

prototype属性是函数所独有的。但是由于JS中函数也是一种对象,所以函数也拥有__proto__constructor属性

2)原型链

通过以上我们知道: 函数可以有属性,每个函数都有一个特殊的属性叫作原型:prototype

原型对象有一个自有属性constructor,这个属性指向该函数

对象有属性__proto__,指向它的构造函数的原型对象

而原型对象作为对象自然也拥有原型,于是,对象实例和它的构造器直接通过 proto 属性建立一个链接,一层层的最终形成原型链

( proto 属性是从构造函数的prototype属性派生的)

即:__proto__作为不同对象之间的桥梁,用来指向创建它的构造函数的原型对象的

3)经典原型链图

4)例题理解

// 一, 注意这里getName是个没有声明的变量,会自动加到全局即window中
function test() {           
    getName = function() { 
        Promise.resolve().then(() => console.log(0,"timeout-1")); 
        console.log(1);               
    };

    return this; 
}

// 二, 未声明,相当于全局定义
test.getName = function() { 
     setTimeout(() => console.log(2, "timeout-2"), 0); 
     console.log(3);               
};

// 三, 定义在原型对象上,注意这个test是函数,因为只有函数有prototype
test.prototype.getName = function() {    
     console.log(4); 
};  

// 四, 函数声明会在变量前,而同名的变量又会覆盖函数声明
var getName = function() { 
     console.log(5);             
};

// 五, 这个函数声明其实不会不会起作用,因为被覆盖了
function getName() {
     console.log(6); 
}      
      
test.getName(); // 全局调用,相当于调用了 二
getName();  // 相当于调用了 四

test().getName(); 
// test()表示函数调用,所以先执行一, 一中返回的this,在没有调用对象的情况下,就是window
// 所以相当于调用了window.getName(),又由于test中的getName没有var声明,所以自动注册到window中
// 将四 覆盖,故此调用成立,输出 '1'

getName();  // 调用test()后,覆盖了之前的getName,所以输出 '1'

new test.getName(); // 就是调用了二
new test().getName(); 
// new test() 返回个空对象,没有getName方法,所以去原型上找,所以最后调用了三
new new test().getName(); // 同理,调用了三,但是三无返回值,返回的也是空对象

// 解析
test.getName(); test

9、作用域链

1)作用域

即变量(变量作用域又称上下文)和函数生效(能被访问)的区域或集合,换句话说,作用域决定了代码区块中变量和其他资源的可见性

作用域一般分为:

  • 全局作用域
  • 函数作用域
  • 块级作用域

2)词法作用域

静态作用域,变量被创建时就确定好了,而非执行阶段确定的

3)作用域链

当在Javascript中使用一个变量的时候,首先Javascript引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域

如果在全局作用域里仍然找不到该变量,它就会在全局范围内隐式声明该变量(非严格模式下)或是直接报错

函数在执行的过程中,先从自己内部寻找变量,如果找不到,再从创建当前函数所在的作用域去找,从此往上,也就是向上一级找,直到找到全局作用域


10、this指向问题

this 关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对象

1)默认绑定

如果一个函数中有this,但是它没有被上一级的对象所调用,那么this指向的就是window

注:严格模式下,不能将全局对象用于默认绑定,this会绑定到undefined,只有函数运行在非严格模式下,默认绑定才能绑定到全局对象

2)隐式绑定

函数还可以作为某个对象的方法调用,这时this就指这个上级对象

即:

如果一个函数中有this,这个函数有被上一级的对象所调用,那么this指向的就是上一级的对象。

如果一个函数中有this这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它 上一级 的对象(只是上一级)

this永远指向的是最后调用它的对象

3)new绑定

通过构建函数new关键字生成一个实例对象,此时this指向这个实例对象

4)显示修改this指向

apply()、call()、bind()是函数的一个方法,作用是改变函数的调用对象。它的第一个参数就表示改变后的调用这个函数的对象。因此,这时this指的就是这第一个参数

简言之:

  • 在全局作用域中:this -> window

  • 在普通函数中:this取决于谁调用,谁调用我,this就指向谁,跟如何定义无关

5)箭头函数中的this

MDN:在箭头函数中,this与封闭词法环境的this保持一致。

  1. 箭头函数没有自己的this
  2. 箭头函数的this就是上下文中定义的this
  3. 箭头函数没有自己的this,不能用做构造函数。
  4. 箭头函数的this在编译的时候就确定了this的指向

(技巧:因为javascript中除了全局作用域,其他作用域都是由函数创建出来的,所以如果想确定this的指向,则找到离箭头函数最近的function,与该function平级的执行上下文中的this即是箭头函数中的this)

理解: 箭头函数中所使用的this都是来自函数作用域链,它的取值遵循普通普通变量一样的规则,在函数作用域链中一层一层往上找

6)事件绑定中的this

- 事件源.onclik = function(){ } //this -> 事件源

- 事件源.addEventListener(function(){ }) //this -> 事件源

7)定时器中的this

定时器中的this->window,因为定时器中采用回调函数作为处理函数,而回调函数的this->window

可以通过保存上下文中的this,或者使用箭头函数来解决这个问题

8)构造函数中的this

构造函数配合new使用, 而new关键字会将构造函数中的this指向实例化对象,所以构造函数中的this->实例化对象

9)其他:

优先级:

new绑定优先级 > 显示绑定优先级 > 隐式绑定优先级 > 默认绑定优先级


11、JS事件模型:事件冒泡和事件捕获

1) 事件流:

  • 事件捕获阶段(capture phase)
  • 处于目标阶段(target phase)
  • 事件冒泡阶段(bubbling phase)

2) 事件模型:

(1)原始事件模型(DOM0)

事件绑定监听函数比较简单, 有两种方式:

  • HTML代码中绑定(如:onclicek="function()"
  • 通过JS代码绑定(如:document.getElementById('.btn'); btn.onclick = fun;

特性:

  • 绑定速度快
  • 只支持冒泡、不支持捕获
  • 同一类型事件只能绑定一次

(2)标准事件模型(DOM2)

过程:

  1. 事件捕获阶段:事件从document一直向下传播到目标元素, 依次检查经过的节点是否绑定了事件监听函数,如果有则执行
  2. 事件处理阶段:事件到达目标元素, 触发目标元素的监听函数
  3. 事件冒泡阶段:事件从目标元素冒泡到document, 依次检查经过的节点是否绑定了事件监听函数,如果有则执行

方式:

监听:addEventListener(eventType, handler, useCapture)

移除:removeEventListener(eventType, handler, useCapture)

参数:

eventType指定事件类型(不要加on)

handler是事件处理函数

useCapture是一个boolean用于指定是否在捕获阶段进行处理,

一般设置为false与IE浏览器保持一致

每个处理程序都可以访问 event 对象的属性:

  • event.target —— 引发事件的层级最深的元素。
  • event.currentTarget(=this)—— 处理事件的当前元素(具有处理程序的元素)
  • event.eventPhase —— 当前阶段(capturing=1,target=2,bubbling=3)。

3) 事件捕获(event capturing):

通俗的理解就是,当鼠标点击或者触发dom事件时,浏览器会从根节点开始由外到内进行事件传播,即点击了子元素,如果父元素通过事件捕获方式注册了对应的事件的话,会先触发父元素绑定的事件。

4) 事件冒泡(dubbed bubbling):

与事件捕获恰恰相反,事件冒泡顺序是由内到外进行事件传播,直到根节点。 支持冒泡的事件如下表

Event Type(事件类型)Bubbling phase(冒泡)
abort
beforeinput
blur
click
compositionstart
compositionupdate
compositionend
dblclick
error
focus
focusin
focusout
input
keydown
keyup
load
mousedown
mouseenter
mouseleave
mousemove
mouseout
mouseover
mouseup
resize
scroll
select
unload
wheel

5) 自定义事件

(1)创建

let event = new Event(type[, options]);

参数:

type —— 事件类型,可以是像这样 "click" 的字符串,或者我们自己的像这样 "my-event" 的参数。

options —— 具有两个可选属性的对象:

bubbles: true/false —— 如果为 true,那么事件会冒泡。

cancelable: true/false —— 如果为 true,那么“默认行为”就会被阻止。稍后我们会看到对于自定义事件,它意味着什么。

默认情况下,以上两者都为 false:{bubbles: false, cancelable: false}

(2)使用

elem.dispatchEvent(event)调用在元素上“运行”它

有一种方法可以区分“真实”用户事件和通过脚本生成的事件。

对于来自真实用户操作的事件,event.isTrusted 属性为 true,对于脚本生成的事件,event.isTrusted 属性为 false。

注意:

  1. 我们应该对我们的自定义事件使用 addEventListener,因为 on<event> 仅存在于内建事件中,
  2. 必须设置 bubbles:true,否则事件不会向上冒泡。
  3. 对于我们自己的全新事件类型,例如 "hello",我们应该使用 new CustomEvent 从技术上讲,CustomEvent 和 Event 一样。除了一点不同。 在第二个参数(对象)中,可以为我们想要与事件一起传递的任何自定义信息添加一个附加的属性 detail

6) 鼠标事件

(1)移动

  • 快速移动鼠标可能会跳过中间元素。
  • mouseover/outmouseenter/leave 事件还有一个附加属性:relatedTarget。这就是我们来自/到的元素,是对 target 的补充。
  • 即使我们从父元素转到子元素时,也会触发 mouseover/out 事件。浏览器假定鼠标一次只会位于一个元素上 —— 最深的那个。
  • mouseenter/leave 事件在这方面不同:它们仅在鼠标进入和离开元素时才触发。并且它们不会冒泡

注:两者不同

  • mouseover:当鼠标移入元素或其子元素都会触发事件,所以有一个重复触发,冒泡的过程。对应的移除事件是mouseout
  • mouseenter:当鼠标移除元素本身(不包含元素的子元素)会触发事件,也就是不会冒泡,对应的移除事件是mouseleave

(2)拖放

基础的的拖放算法:

  1. mousedown 上 —— 根据需要准备要移动的元素(也许创建一个它的副本,向其中添加一个类或其他任何东西)。
  2. 在拖动开始时 —— 记住鼠标指针相对于元素的初始偏移(shift):shiftX/shiftY,并在拖动过程中保持它不变。
  3. 使用 document.elementFromPoint 检测鼠标指针下的 “droppable” 的元素。
  4. 然后在 mousemove 上,通过更改 position:absolute 情况下的 left/top 来移动它。
  5. mouseup 上 —— 执行与完成的拖放相关的所有行为。

(3)滚动

scroll 事件允许对页面或元素滚动作出反应

我们不能通过在 onscroll 监听器中使用 event.preventDefault() 来阻止滚动,因为它会在滚动发生 之后 才触发。

但是我们可以在导致滚动的事件上,例如在 pageUp pageDown keydown 事件上,使用 event.preventDefault() 来阻止滚动。

如果我们向这些事件中添加事件处理程序,并向其中添加 event.preventDefault(),那么滚动就不会开始。


12、typeof和instanceof

1)typeof 实现原理

js 在底层存储数据类型的方式

  • 000:对象
  • 010:浮点数
  • 100:字符串
  • 110:布尔
  • 1:整数
  • null:所有机器码均为0
  • undefined:用 −2^30 整数来表示

引申null 有属于自己的类型 Null,而不属于Object类型,typeof 之所以会判定为 Object 类型,是因为JavaScript 数据类型在底层都是以二进制的形式表示的,二进制的前三位为 0 会被 typeof 判断为对象类型,而 null 的二进制位恰好都是 0 ,因此,null 被误判断为 Object 类型

2)instanceof 实现原理

验证左边待验证对象的原型链,逐个与右边的原型进行对比,直到比较结果为真或达到原型链末端

function instance_of(left, right) {
  const RP = right.prototype; // 构造函数的原型
  while(true) {
    if (left === null) {
      return false;
    }
    if (left === RP) { // 一定要严格比较
      return true;
    }
    left = left.__proto__; // 沿着原型链重新赋值
  }
}

13、Arguments对象

1、arguments 是一个对应于传递给函数的参数的类数组对象,它除了length属性和索引元素之外没有任何Array属性

2、arguments对象可以与剩余参数、默认参数和解构赋值参数结合使用。

注意:

在严格模式下,剩余参数、默认参数和解构赋值参数的存在不会改变 arguments对象的行为,

但是,在非严格模式下

  • 当非严格模式中的函数没有包含剩余参数、默认参数和解构赋值,那么arguments对象中的值会跟踪参数的值(反之亦然,改变参数值也会改变arguments对象中的值)
  • 当非严格模式中的函数有包含剩余参数、默认参数和解构赋值,那么arguments对象中的值不会跟踪参数的值(反之亦然)。相反, arguments反映了调用时提供的参数

14、JS的模块化

1)CommonJS

动态,运行时加载,一般用于服务端

  1. CommonJS是一种同步加载模块的方式,也就是说,只有当模块加载完成后,才能执行后面的操作

  2. CommonJS每个模块内部都有一个module变量,代表当前模块;这个变量是一个对象,它的exports属性(即module.exports)提供对外导出模块的接口。

  3. require的基本功能是读取并执行JS文件,并返回模块导出的module.exports对象:

  4. CommonJS的加载机制是,模块输出的是一个值的复制拷贝;

  • 对于基本数据类型的输出,属于复制,

  • 对于复杂数据类型,属于浅拷贝,对于复杂数据类型,由于CommonJS进行了浅拷贝,因此如果两个脚本同时引用了同一个模块,对该模块的修改会影响另一个模块

特点:

  • 所有代码都运行在模块作用域,不会污染全局作用域
  • 模块是同步加载的,即只有加载完成,才能执行后面的操作
  • 模块在首次执行后就会缓存,再次加载只返回缓存结果,如果想要再次执行,可清除缓存
  • require返回的值是被输出的值的拷贝,模块内部的变化也不会影响这个值
// a.js
module.exports={ foo , bar}

// b.js
const { foo,bar } = require('./a.js')

2)AMD

AMD,即 (Asynchronous Module Definition),异步加载模块,先定义所有依赖,然后在加载完成后的回调函数中执行: 代表库:requireJs

/** main.js 入口文件/主模块 **/
// 首先用config()指定各模块路径和引用名
require.config({
  baseUrl: "js/lib",
  paths: {
    "jquery": "jquery.min",  //实际路径为js/lib/jquery.min.js
    "underscore": "underscore.min",
  }
});
// 执行基本操作
require(["jquery","underscore"],function($,_){
  // some code here
});

AMD虽然实现了异步加载,但是开始就把所有依赖写出来是不符合书写的逻辑顺序的

3)CMD

CMD (Common Module Definition), 是seajs推崇的规范,CMD则是依赖就近,用的时候再require

define(function(require, exports, module) {
   var clock = require('clock');
   clock.start();
});

AMD和CMD最大的区别是对依赖模块的执行时机处理不同,而不是加载的时机或者方式不同,二者皆为异步加载模块。 AMD依赖前置,js可以方便知道依赖模块是谁,立即加载; CMD就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了那些模块,

4)ES6 module

静态,编译时就能够确定模块之间的依赖关系。

因为ES6输出的则是对外接口,所以会动态更新,会影响导入模块引入的值

export default其实是语法糖,本质上是将后面的值赋值给default变量,所以可以将一个值写在export default之后;但是正是由于它是输出了一个 变量,因此它后面不能再跟变量声明语句。

注:支持动态加载,允许将import()作为函数调用,将其作为参数传递给模块的路径。

它返回一个 promise,它用一个模块对象来实现,让你可以访问该对象的导出


15、js的apply和call方法

相同点:

call()apply()方法的相同点就是这两个方法的作用是一样的。都是在特定的作用域中调用函数,等于设置函数体内this对象的值,以扩充函数赖以运行的作用域

不同点:

call()apply()的不同点就是接收参数的方式不同。

  • apply()方法接收两个参数,一个是函数运行的作用域(this),另一个是参数数组。
  • call()方法不一定接受两个参数,第一个参数也是函数运行的作用域(this),但是传递给函数的参数必须列举出来。

16、JS的严格模式和非严格模式

最常用和常见的是,全局模式下。this不会默认绑定到window,而是undefined

区别:

1)过失错误抛出异常

  1. 无法再意外创建全局变量(某些情况下拼写错误导致全局对象新增一个属性)
  2. 会使引起静默失败(silently fail,注:不报错也没有任何效果)的赋值操作抛出异常
    其他如(给不可写属性赋值, 给只读属性(getter-only)赋值, 给不可扩展对象(non-extensible object)的新属性赋值) 都会抛出异常:
  3. 试图删除不可删除的属性时会抛出异常
  4. 对象内的所有属性名在对象内必须唯一
  5. 函数的参数名唯一
  6. 禁止八进制数字语法
  7. 禁止设置primitive值的属性

2)简化变量的使用

  1. 禁用 with
  2. 严格模式下的 eval 不再为上层范围(surrounding scope,注:包围eval代码块的范围)引入新变量
  3. 禁止删除声明变量。delete name 在严格模式下会引起语法错误

3)eval和arguments

  1. 名称 eval 和 arguments 不能通过程序语法被绑定(be bound)或赋值.
  2. 参数的值不会随 arguments 对象的值的改变而变化,修改值不会互相改变
  3. 不再支持 arguments.callee

4)安全的JS

  1. 在严格模式下通过this传递给一个函数的值不会被强制转换为一个对象
  2. 在严格模式中再也不能通过广泛实现的ECMAScript扩展“游走于”JavaScript的栈中
  3. 严格模式下的arguments不会再提供访问与调用这个函数相关的变量的途径

5)为未来的ES预留

  1. 一部分字符变成了保留的关键字。这些字符包括 implements, interface, let, package, private, protected, public, static和yield

  2. 严格模式禁止了不在脚本或者函数层面上的函数声明

  3. 严格模式下,不允许使用with。

  4. 严格模式下,不允许给未声明的变量赋值

  5. 严格模式下,arguments变为参数的静态副本。非严格模式下,arguments对象里的元素和对应的参数是指向同一个值的引用,但是:传的参数是对象除外。arguments和形参共享传递

  6. 严格模式下,删除参数名,函数名报错。非严格模式返回false,静默失败。(静默失败:不报错也没有任何效果)

  7. 严格模式下,函数参数名重复报错。非严格模式最后一个重名参数会覆盖之前的重名参数。

  8. 严格模式下,删除不可配置(configurable=false)的属性报错。非严格模式返回false,静默失败

  9. 严格模式下,修改不可写(writable=false)的属性报错。

  10. 严格模式下,对象字面量重复属性名报错。

  11. 严格模式下,禁止八进制字面量。

  12. 严格模式下,eval,arguments成为关键字,不能用作变量,函数名。

  13. 严格模式下,eval变成了独立作用域。

  14. 严格模式下,给只读属性赋值报错。

  15. 严格模式下,给不可扩展对象的新属性赋值报错。

  16. ES6中,严格模式下,禁止设置五种基本类型值的属性。

  17. 严格模式下,一般函数调用(不是对象的方法调用,也不使用apply/call/bind等修改this),this指向undefined,而不是全局对象。

  18. 严格模式下,使用apply/call/bind,当传入参数是null/undefined时,this指向null/undefined,而不是全局对象。

  19. 严格模式下,不再支持arguments.callee。非严格模式下,arguments.callee指向当前正在执行的函数

  20. 严格模式下,不再支持arguments.caller

  21. 严格模式下,禁止了不在脚本或者函数层面上的函数声明。


17、JS的全局函数

URI操作

  • decodeURI() 解码某个编码的 URI。
  • decodeURIComponent() 解码一个编码的 URI 组件。
  • encodeURI() 把字符串编码为 URI。
  • encodeURIComponent() 把字符串编码为 URI 组件。

字符串编码/解码

  • escape() 对字符串进行编码。
  • unescape() 对由 escape()编码的字符串进行解码。

其他常用

  • eval() 计算 JavaScript 字符串,并把它作为脚本代码来执行。返回最后一个执行结果

  • isFinite() 检查某个值是否为有穷大的数。

  • isNaN() 检查某个值是否是数字。

  • Number() 把对象的值转换为数字。

  • String() 把对象的值转换为字符串。

  • parseFloat() 解析一个字符串并返回一个浮点数。

  • parseInt() 解析一个字符串并返回一个整数。


18、parseInt和toString

1)parseInt

  1. 语法:parseInt(string, radix);

  2. 返回值 从给定的字符串中解析出的一个整数。或者 NaN,当

  • radix 小于 2 或大于 36
  • 第一个非空格字符不能转换为数字。
  1. radix不传时

    如果 radix 是 undefined0未指定的,JavaScript会假定以下情况:

  • 如果输入的 string"0x""0X"开头,那么radix被假定为16,字符串的其余部分被当做十六进制数去解析。

  • 如果输入的 string"0"(0)开头, radix被假定为8(八进制)或10(十进制)。具体选择哪一个radix取决于实现。ECMAScript 5 澄清了应该使用 10 (十进制),但不是所有的浏览器都支持。因此,在使用 parseInt 时,一定要指定一个 radix。

  • 如果输入的 string 以任何其他值开头, radix10 (十进制)。

  1. string的数字大于radix时(如下:7>6),它会只解析到它的上一位,如:

    parseInt('17',6) = parseInt('1',6) = 1;

    如果只有一位,那返回NaN

  2. 例题:

console.log(['1', '2', '3'].map(parseInt))
// [1, NaN, NaN]

// map传入第一个参数为当前值,第二个为索引,会传入parseInt
// 故:
// parseInt('1', 0) = parseInt('1', 10) = 1
// parseInt('2', 1) radix最小为2,最大为36,所以NaN
// parseInt('3', 2) 3大于基数2,不在范围内,所以NaN

2)toString

数字的toString方法可以传入一个radix,转换成对应进制字符串

但是注意,不能直接对数字调用 toString(radix)方法,会报错

let num = 255
num.toString(16) // 'ff'
255.toStrinmg(16) // Uncaught SyntaxError: Invalid or unexpected token

19、JS的运算符优先级

JS运算符优先级

三元运算符的优先级很低,差不多仅高于赋值和逗号

new > 后置++/-- > 前置的运算符或符号 > 算术运算符 >位移动> 逻辑运算符 > 与或非等> 三元 > 赋值 > 逗号


20、关于对象、字符、数字间的相加

对象与数字相加时,对象调用自身的valueOf方法转换为数字

当对象没有提供valueOf方法时,对象与数字都转换为字符串相加,对象转为 "[object Object]" 字符串

{} + 1 // 1
1 + {} // '1[object Object]'

[] + 1 // '1'
1 + [] // '1'

21、类型的比较和判断

1)'==''==='

注:null是一个表示"无"的对象,转为数值时为0

undefined是一个表示"无"的原始值,转为数值时为NaN

0 == NaN // false
0 == '' // true
0 == [] // true
0 == undefined // false

'' == [] // true

false == '' // true

// 以上图几个关键点
NaN == NaN // false
+0 == -0 // true

null == undefined // true

// 字符串与数字比较,先将字符串转为数字
// 字符串或数字与对象比较,先将对象转为原始类型

2)Object.is(val1, val2)

Object.is()方法判断两个值是否为同一个值。如果满足以下条件则两个值相等:

  • 都是 undefined
  • 都是 null
  • 都是 truefalse
  • 都是相同长度的字符串且相同字符按相同顺序排列
  • 都是相同对象(意味着每个对象有同一个引用)
  • 都是数字且
    • 都是 +0
    • 都是 -0
    • 都是 NaN
    • 或都是非零而且非 NaN 且为同一个值

== 运算不同。 == 运算符在判断相等前对两边的变量(如果它们不是同一类型) 进行强制转换 (这种行为的结果会将 "" == false 判断为 true), 而 Object.is不会强制转换两边的值。

===运算也不相同。 === 运算符 (也包括 == 运算符) 将数字 -0+0 视为相等 ,而将NaN NaN视为不相等.

3)其他细节

NaN == NaN // false

NaN === NaN // false

indexOf方法无法识别数组的NaN成员 但是Set、includes、Object.is()都会认为NaN == NaN


22、根据 0.1+0.2 ! == 0.3,讲讲 IEEE 754 ,如何让其相等?

1)原因总结:

  • 进制转换 :js 在做数字计算的时候,0.1 和 0.2 都会被转成二进制后无限循环 ,但是 js 采用的 IEEE 754 二进制浮点运算,最大可以存储 53 位有效数字,于是大于 53 位后面的会全部截掉,将导致精度丢失。

  • 对阶运算 :由于指数位数不相同,运算时需要对阶运算,阶小的尾数要根据阶差来右移(0舍1入),尾数位移时可能会发生数丢失的情况,影响精度。

// 0011无限循环
0.1 = 0.00011(0011) = 2^-4 * 1.1(0011)

// 因此
0.100000000000000002 === 0.1 // true
0.200000000000000002 === 0.2 // true
0.1 + 0.2 = 0.300000000000000004 !==0.3

2)解决方法:

  1. 转为整数(大数)运算:使用toFixed
function compareNum(num1,num2){
    return num1.toFixed(10) === num2.toFixed(10)
}
console.log(compareNum(0.1+0.2,0.3)) // true
  1. 使用 Number.EPSILON 误差范围
function compareNum(num1,num2){
    return Math.abs(num1-num2)<Number.EPSILON
}
console.log(compareNum(0.1+0.2,0.3)) // true
  1. 转成字符串,对字符串做加法运算。

23、JSON.stringify()

语法:

let json = JSON.stringify(value[, replacer, space])`

// replacer: 要编码的属性数组或映射函数 `function(key, value)`

// space: 用于格式化的空格数量

JSON.stringify()将值转换为相应的JSON格式:

JSON 支持以下数据类型:

  • Objects { ... }
  • Arrays [ ... ]
  • Primitives
    • strings
    • numbers
    • boolean values true/false
    • null

一些特定于 JavaScript 的对象属性会被 JSON.stringify 跳过。

即:

  • 函数属性(方法)。
  • Symbol 类型的键和值。
  • 存储 undefined 的属性

重要的限制:不得有循环引用。

总结:

  • 转换值如果有 toJSON() 方法,该方法定义什么值将被序列化。
  • 非数组对象的属性不能保证以特定的顺序出现在序列化后的字符串中。
  • 布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。
  • undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。函数、undefined 被单独转换时,会返回 undefined,如JSON.stringify(function(){}) or JSON.stringify(undefined).
  • 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
  • 所有以 symbol 为属性键的属性都会被完全忽略掉,即便 replacer 参数中强制指定包含了它们。
  • Date 日期调用了 toJSON() 将其转换为了 string 字符串(同Date.toISOString()),因此会被当做字符串处理。
  • NaN Infinity 格式的数值及 null 都会被当做 null
  • 其他类型的对象,包括 Map/Set/WeakMap/WeakSet仅会序列化可枚举的属性

自定义toJSON()方法


24、区分数组和对象

  1. Array.isArray
  2. instanceof
  3. Object.prototype.toString.call
  4. constructor

25、js的连等赋值思考

例题1:

因为输出的a还是全局变量a,而b是全局变量,在函数内被改变了

var a = b = 10;
(function(){
   var a = b = 20;
})();
console.log(a);
console.log(b);
// 问:输出的 a = ?    b = ?
// 答:a = 10, b = 20

例题2:

a.x = a = {n:2};

先创建一个对象{n:2}并把他的引用传给变量a。因为a.x在执行前保留了对{n:1}的引用,所以给原对象增加一个属性a即对{n:2}的引用

a现在为{n:2}并没有x的属性所以为undefined,而b仍然指向原来的引用,所以b.x = a;

var a = {n:1}; 
var b = a;
a.x = a = {n:2};

console.log(a);	// {n: 2}
console.log(b);	// {n: 1, x: {n: 2}}
console.log(a.x); // undefined
console.log(b.x); // {n: 2}

关键点:

  1. 连等操作符是从右向左的
  2. 函数内部声明会屏蔽外部
  3. 未声明直接赋值会产生额外的全局变量
  4. 是否保留源对象的引用

26、js内存泄露

  1. 意外的全局变量;
  2. 闭包;
  3. 未被清空的定时器;
  4. 未被销毁的事件监听;
  5. DOM 引用;

27、JS垃圾回收机制

标记清除

当代码执行在一个环境中时,每声明一个变量,就会对该变量做一个标记(例如标记一个进入执行环境);当代码执行进入另一个环境中时,也就是要离开上一个环境,这时对上一个环境中的变量做一个标记,(例如标记一个离开执行环境),等到垃圾回收执行时,会根据标记来决定要清除哪些变量进行释放内存

引用计数

引用计数是一种不太常用的垃圾回收方式。 引用计数的策略是跟踪记录每个值被使用的次数,当声明了一个变量并将一个引用类型赋值给该变量的时候这个值的引用次数就加1,如果该变量的值变成了另外一个,则这个值的引用次数减1,当这个值的引用次数变为0的时候,说明没有变量在使用,这个值没法被访问了,因此可以将其占用的空间回收,当垃圾回收的时候,就会将 引用次数为0的进行回收,释放对应的内存


28、delete使用原则

  1. delete使用原则:delete 操作符用来删除一个对象的属性。

  2. delete在删除一个不可配置的属性时在严格模式和非严格模式下的区别:

    (1)在严格模式中,如果属性是一个不可配置(non-configurable)属性,删除时会抛出异常;

    (2)非严格模式下返回 false

  3. delete能删除隐式声明的全局变量:这个全局变量其实是global对象(window)的属性

  4. delete能删除的:

    (1)可配置对象的属性

    (2)隐式声明的全局变量

    (3)用户定义的属性

    (4)在ES6中,通过 constlet 声明指定的 "temporal dead zone" (TDZ) 对 delete 操作符也会起作用

  5. delete不能删除的:

    (1)显式声明的全局变量

    (2)内置对象的内置属性

    (3)一个对象从原型继承而来的属性

  6. delete删除数组元素:

    (1)当你删除一个数组元素时,数组的 length 属性并不会变小,数组元素变成undefined

    (2)当用 delete 操作符删除一个数组元素时,被删除的元素已经完全不属于该数组。

    (3)如果你想让一个数组元素的值变为 undefined 而不是删除它,可以使用 undefined 给其赋值而不是使用 delete 操作符。此时数组元素是在数组中的


29、严格模式

开启:"use strict"

1)过失错误抛出异常

  1. 无法再意外创建全局变量(某些情况下拼写错误导致全局对象新增一个属性)
  2. 会使引起静默失败(silently fail,注:不报错也没有任何效果)的赋值操作抛出异常 其他如(给不可写属性赋值, 给只读属性(getter-only)赋值, 给不可扩展对象(non-extensible object)的新属性赋值) 都会抛出异常:
  3. 试图删除不可删除的属性时会抛出异常
  4. 对象内的所有属性名在对象内必须唯一
  5. 函数的参数名唯一
  6. 禁止八进制数字语法 (不要使用前导零,以0开头)
  7. 禁止设置primitive值的属性

2)简化变量的使用

  1. 禁用 with
  2. 严格模式下的 eval 不再为上层范围(surrounding scope,注:包围eval代码块的范围)引入新变量
  3. 禁止删除声明变量。delete name 在严格模式下会引起语法错误

3)eval和arguments

  1. 名称 eval 和 arguments 不能通过程序语法被绑定(be bound)或赋值.
  2. 参数的值不会随 arguments 对象的值的改变而变化,修改值不会互相改变
  3. 不再支持 arguments.callee

4)安全的JS

  1. 在严格模式下通过this传递给一个函数的值不会被强制转换为一个对象
  2. 在严格模式中再也不能通过广泛实现的ECMAScript扩展“游走于”JavaScript的栈中
  3. 严格模式下的arguments不会再提供访问与调用这个函数相关的变量的途径