对象、原型、函数和闭包的紧密结合组成了JavaScript。本文会从函数,对象和浏览器三个方面全面梳理Javascript 知识。结合《javascript 忍者秘籍》和《你不知道的javascript》整理理解 JS。并且按逻辑总结出常见的 JS 面试题,可以通过目录快速定位查看,持续更新~ 🔗
1.基本概念
Javascript 能够运行在很多环境中,这里将重点考虑浏览器环境。
🚀面试题:web应用生命周期
🚀面试题:JS事件循环
2.理解函数
函数是 javascript 的基础,函数是第一类对象,或者说被称为一等公民。函数和那些 更普通的JavaScript数据类型一样,它能被变量引用,能以字面量形式声明,甚至能被作为函数参数进行传递。通过以下几个部分让我们对函数有一个全面的了解吧。
入门:定义与参数
基础:函数调用和上下文
进阶:理解闭包与作用域
高级:生成器和Promise
2.1 函数入门:定义与参数
函数是程序执 行过程中的主要模块单元。除了全局JavaScript代码是在页面构建的阶段 执行的,我们编写的所有的脚本代码都将在一个函数内执行。 我们知道函数本质上也是一种特殊的对象,因此在 javascript 中函数还具有一些对象的特性。
🚀面试题:对象的常用功能
- 对象可以通过字面量来创建{}
const obj = {} - 对象可以赋值给变量、数组项,或其他对象的属性。
- 对象可以作为参数传递
- 对象可以作为函数的返回值
- 对象能够动态创建和分配的属性
对应的我们函数也能支持这些操作
function ninjaFunction() {} // 通过字面量创建
var ninjaFunction = function() {}; // 为变量赋值一个新函数
ninjaArray.push(function(){}); // 向数组中增加一个新函数
ninja.data = function(){}; // 给某个对象的属性赋值为一个新函数
call(function(){}); // 一个新函数作为参数传递给函数
function returnNewNinjaFunction() {
return function(){}; // 返回一个新函数
}
ninjaFunction.ninja = "Hanzo"; //为函数增加一个新属性
🚀面试题:什么是函数式编程,优点是什么
把函数作为第一类对象是函数式编程(functional programming)的第一步,函数式编 程是一种编程风格,它通过书写函数式(而不是指定一系列执行步骤,就像那种更主 流的命令式编程)代码来解决问题。函数式编程可以让代码更容易测试、扩展及模块 化。 对象能做的任何一件事,函数也都能做。函数也是对象,唯一的特殊之处在于它是可调用的(invokable),即函数会被调用以便执行某项动作。
函数能够作为参数传入函数。基于这个特性就有了回调函数的概念
🚀面试题:什么是回调函数
如果我们将某个函数作为参数传入另一个函数, 传入函数会在应用程序执行的未来某个时间点才执行。
例如在事件处理,排序,等一些原生JS方法也会让我们传递回调函数。
JavaScript的重要特征之一是可以在表达式出现的任意位置创建函数,除此之外这种方式能使代码更紧凑和易于理解(把函数定义放在函数使用处附近)。当一个函数不会在代码的多处位置被调用时,该特性可以避免用非必需的名字污染全局命名空间。
🚀面试题:函数的定义方式有哪几种
- 函数声明
function myFun(){ return 1;}
即使定义在后面也可以直接调用
- 函数表达式
var myFun = myFun(){ return 1;}
如果使用 var 定义会被提升,在定义函数表达式之前访问函数名会得到undefined
对于函数声明来说,函数名是强制性的,而对于函数 表达式来说,函数名则完全是可选的。
- 箭头函数,ES6新增,一种简洁的语法
const fun = (a,b)=>{return a+b}
const fun = a => a*2 //如果真有一个参数和直接返回可以这样简写
- 函数构造函数——不太常用,以字符串的形式动态创建函数
new Function('a', 'b', 'return a + b')
- 生成器函数——ES6新增功能,能让我们创建不同于普通函数的函 数,在应用程序执行过程中,这种函数能够退出再重新进入,在这 些再进入之间保留函数内变量的值。我们可以定义生成器版本的函 数声明、函数表达式、函数构造函数。
function* myGen(){ yield 1; }
🚀面试题:什么是形参和实参
- 形参(parameter)是我们定义函数时所列举的变量。
- 实参(argument)是我们调用函数时所传递给函数的值。
🚀面试题: ES6中的剩余参数和默认参数
function multiMax(first, ...remainingNumbers) { //剩余参数以...作前缀
remainingNumbers叫做剩余参数数组,包含传入的剩余参数。
限制:只能放在参数列表的最后
function performAction(ninja, action = "skulking"){
像很多其它的语言一样,可以指定参数的默认值,当为传入参数时,使用默认值
2.2 函数基础:调用和上下文
这部分内容主要包括,理解函数中的两个隐藏参数arguments和this,函数调用的不同方式和函数的上下文问题。
2.2.1 隐式函数参数
arguments参数是传递给函数的所有参数集合。无论是否有明确定义对应的形参,通过它我们都可以访问到函数的所有参数。由于ES6 引入的剩余参数,对于arguments的使用在慢慢减少。
🚀面试题:arguments 和 ES6中剩余参数的区别
- arguments 得到的是所有的参数,而ES6中的剩余参数得到的是为匹配的剩余的参数。
- arguments是一个对象,而剩余参数是一个数组。
- arguments还有一个特性是它是对象的别名,比如修改arguments[0]会直接修改第一个参数的值。在严格模式下会被禁止这一特性
🚀面试题:什么是严格模式
严格模式是在ES5中引入的特性,它可以改变JavaScript引擎的默认行为并执行更加严格的语法检查,一些在普通模式下的静默错误会在严格模式下抛出异常。在严格模式下部分语言特性会被改变,甚至完全禁用一些不安全的语言特性(后面会详细介绍)
2.2.2 this参数
当调用函数时,除了显式提供的参数外,this参数也会默认地传递给函数。this参数是面向对象JavaScript编程的一个重要组成部分,代 函数调用相关联的对象。因此,通常称之为函数上下文。
在JavaScript中,将一个函数作为方法(method)调用仅仅是函数调用的一种方式。事实上,this参数 的指向不仅是由定义函数的方式和位置决定的,同时还严重受到函数调用方式的影响。
我们要清楚一点每个函数的this是在调用时被绑定的
🚀面试题:this的绑定规则
JavaScript中的this只有如下几种情况,并按他们的优先级从低到高划分如下:
- 独立函数调用,例如
getUserInfo(),此时this指向全局对象window - 对象调用,例如
stu.getStudentName(),此时this指向调用的对象stu call()、apply()和bind()改变上下文的方法,this指向取决于这些方法的第一个参数,当第一个参数为null时,this指向全局对象window- 箭头函数没有
this,箭头函数里面的this只取决于包裹箭头函数的第一个普通函数的this new构造函数调用,this永远指向构造函数返回的实例上,优先级最高。
2.2.3 函数调用
事实上,函数的调用方式对函数内代码的执行有 很大的影响,主要体现在this参数以及函数上下文是如何建立的。
🚀面试题:函数的调用方式有哪些?
-
作为一个函数(function)——skulk(),直接被调用。 当以这种方式调用时,函数上下文(this关键字的值)有两种可能 性:在非严格模式下,它将是全局上下文(window对象),而在严格 模式下,它将是undefined。
-
作为一个方法(method)——ninja.skulk(),关联在一个对象上,实现 面向对象编程。
当一个函数被赋值给一个对象的属性,并且通过对象属性引用的方式调用函数时,函数会作为对象的方法被调用。
当函数作为某个对象的方法被调用时,该对象会成为函数的上下文,并且在函数内部可以通过参数访问到。这也是JavaScript实现面向对象编程的主要方式之一。 -
作为一个构造函数(constructor)——new Ninja(),实例化一个新的对象。
构造函数的声明和其他函数类似,通过可以使用函数声明和函数表达式很容易地构造新的对 象。若要通过构造函数的方式调用,需要在函数调用之前使用关键字 new。 -
通过函数的apply或者call方法——skulk.apply(ninja)或者 skulk.call(ninja)。 JavaScript为我们提供了一种调用函数的方式,从而可以显式地指定 任何对象作为函数的上下文。
apply和call正是函数的方法。若想使用apply方法调用函数,需要为其传递两个参数:作为函数上下文的对象和一个数组作为函数调用的参数。call方法的使用方式类 似,不同点在于是直接以参数列表的形式,而不再是作为数组传递。
🚀面试题:使用new调用函数,会执行哪些操作?
- 创建一个全新的空对象
- 新对象会被执行原型绑定
- 新的空对象会被绑定到该函数的this
- 如果函数没有返回其它对象,那么就会自动返回这个新对象
2.2.4 箭头函数
箭头函数和bind方法,在一些情况下可以更优雅地实现相同的效果。
箭头函数没有单独的this值。箭头函数的this与声明所在的上下文的相同。
函数还可访问bind方法创建新函数。无论使用哪种方法 调用,bind方法创建的新函数与原始函数的函数体相同,新函数被绑定 到指定的对象上。
🚀面试题:call(),apply(),bind()的区别
🚀面试题:普通函数与箭头函数的区别
- this指向不同,普通函数的this是在调用时确定,而箭头函数的this在声明时就已经确定
- 箭头函数没有原型prototype
- 箭头函数不能当成一个构造函数
- 箭头函数处于全局作用域中,则没有arguments,处于普通函数的函数作用域中,arguments则是上层普通函数的arguments,可以使用剩余参数列表
- 箭头函数不能重复函数参数名称
- 声明方式不同,箭头函数只能声明成匿名,然后使用表达式的方式指定别名
2.3 函数进阶:闭包和作用域
2.3.1 理解闭包
闭包是基于词法作用域写代码时所产生的自然结果,你甚至不需要为了利用它们而有意识地创建闭包。闭包的创建和使用在你的代码中随处可见。你缺少的是根据自己的意愿来识别、拥抱和影响闭包的思维环境。
闭包允许函数访问并操作函数外部的变量。只要变量或函数存在于声明函数时的作用域内,闭包即可使函数能够访问这些变量或函数。所声明的函数可以在声明之后的任何时间被调用,甚至当该函数声明的作用域消失之后仍然可以调用。
这就是闭包。闭包创建了被定义时的作用域内的变量和函数的安全气泡,因此函数获得了执行时所需的内容。该气泡与函数本身一起包含了函数和变量。
每一个通过闭包 访问变量的函数都具有一个作用域链,作用域链包含闭包的全部信息, 这一点非常重要。因此,虽然闭包是非常有用的,但不能过度使用。使 用闭包时,所有的信息都会存储在内存中,直到JavaScript引擎确保这些 信息不再使用(可以安全地进行垃圾回收)或页面卸载时,才会清理这 些信息。
2.3.2 使用闭包
原生JavaScript不支持私有变量。但是,通过使用闭包,我们可以实现很接近的、可接受的私有变量。
🚀面试题:如何通过闭包实现私有变量
function Ninja() {
var feints = 0;
this.getFeints = function() {
return feints;
};
this.feint = function() {
feints++;
};
}
在构造器内部,我们定义了一个变量feints用于保存状态。由JavaScript的作用域规则的限制,因此只能在构造器内部访问该变量。为了让作用域外部的代码能够访问该变量,我们定义了访问该变量的方法 getFeints。该方法可以读取私有变量,但不能改写私有变量。
2.3.3 函数执行上下文
JavaScript代码有两种类型:一种是全局代码,在所有函数外部定义;一种是函数代码,位于函数内部。JavaScript 引擎执行代码时,每一条语句都处于特定的执行上下文中。
既然具有两种类型的代码,那么就有两种执行上下文:全局执行上下文和函数执行上下文。二者最重要的差别是:全局执行上下文只有一个,当JavaScript程序开始执行时就已经创建了全局上下文;而函数执行上下文是在每次调用函数时,就会创建一个新的。
2.3.4 使用词法环境跟踪变量的作用域
词法环境是JavaScript作用域的内部实现机制,人们通常称为作用域(scopes)。
词法环境(lexical environment)是JavaScript引擎内部用来跟踪标识 符与特定变量之间的映射关系。
词法环境与特定的JavaScript代码结构关联,既可以是一个函数、一段代码片段,也可以是try-catch语句。这些代码结构(函数、代码片段、try-catch)可以具有独立的标识符映射表。
对于下列函数
无论何时调用函数,都会创建一个新的执行环境,被推入执行上下文栈。此外,还会创建一个与之相关联的词法环境。现在来看最重要的部分:外部环境与新建的词法环境,JavaScript引擎将调用函数的内置
[[Environment]]属性与创建函数时的环境进行关联。类似原型链[[Prototype]]的关联。
🚀面试题:let,var和const的区别
var声明的变量会提升到作用域的顶部,而let和const不会进行提升var声明的全局变量会被挂载到全局window对象上,而let和const不会var可以重复声明同一个变量,而let和const不可以var声明的变量作用域范围是函数作用域,而let和const声明的变量作用域范围是块级作用域。const声明的常量,一旦声明则不能再次赋值,再次赋值会报错(更改对象属性不会,因为对象地址没有变)
2.4 函数高级:生成器和promise
2.4.1 生成器
通过在function关键字后增加一个*号可以定义生成器,生成器(generator)函数能生成一组值的序列,但每个值的 生成是基于每次请求,并不同于标准函数那样立即生成。
我们必须显式地向生成器请求一个新的值,随后生成器要么响应一个新生成的值,要么就告诉我们它之后都不会再生成新值。
生成器函数和标准函数非常不同,调用生成器并不会执行生成器函数,相反,它会创建一个叫作迭代器(iterator) 的对象。
使用Promise
ES6 引入了一个新的概念,用 于更简单地处理异步任务:promise。
promise对象用于作为异步任务结果的占位符。它代表了一个我们 暂时还没获得但在未来有希望获得的值。基于这点原因,在一个 promise对象的整个生命周期中,它会经历多种状态。一个promise对象从等待(pending)状态开始,此时我们对承诺的值一无所知。因此一个等待状态的promise对象也称为未实现(unresolved)的 promise。在程序执行的过程中,如果promise的resolve函数被调用, promise就会进入完成(fulfilled)状态,在该状态下我们能够成功获取到承诺的值。
3.理解对象
4.理解浏览器环境
文档对象模型(Document Object Model),简称 DOM,将所有页面内容表示为可以修改的对象。
浏览器对象模型(Browser Object Model),简称 BOM,表示由浏览器(主机环境)提供的用于处理文档(document)之外的所有内容的其他对象。
4.1文档对象模型DOM
HTML 文档对主干是标签,每个标签都是一个对象,标签内还可以嵌套子标签,也是一个对象。
我们可以就通过 JS 来访问这些对象,例如更改背景颜色document.body.style.background = 'red'; // 将背景设置为红色
对 DOM 的所有操作都是以 document 对象开始。它是 DOM 的主“入口点”。
4.1.1 遍历 DOM
- 子节点 对应的是直系的子元素。换句话说,它们被完全嵌套在给定的元素中。。
- 子孙元素 嵌套在给定元素中的所有元素,包括子元素,以及子元素的子元素等。
const cs = document.body.childNodes // 获取body的所有子元素
document.body.firstChild // 第一个子元素
document.body.lastChild // 最后一个子元素
document.body.hasChildNodes() // 检查是否有子元素
cs 是可迭代对象,而不是一个数组。可以使用for...of遍历,而不可可以使用 for...in
在 childNodes 中我们可以看到文本节点,元素节点,甚至包括注释节点(如果它们存在的话)。
- 兄弟节点(sibling)具有相同的父节点
- 父节点元素的父节点
document.head.nextSibling // 下一个兄弟节点
document.body.previousSibling // 上一个兄弟节点
document.body.parentNode // 父节点
- 纯元素形成页面结构的元素节点(不包括文本、注释等)只需要在上面等方法中加入
Elementchildren—— 仅那些作为元素节点的子代的节点。firstElementChild,lastElementChild—— 第一个和最后一个子元素。previousElementSibling,nextElementSibling—— 兄弟元素。parentElement—— 父元素。
4.1.2 搜索 DOM
document.getElementById(id)获取指定id的元素(id唯一)elem.getElementsByTagName(tag)查找具有给定标签的元素,并返回它们的集合。elem.getElementsByClassName(className)返回具有给定CSS类的元素。document.getElementsByName(name)返回在文档范围内具有给定name特性的元素。- 最通用的方法是
elem.querySelectorAll(css),它返回elem中与给定 CSS 选择器匹配的所有元素。 elem.querySelector(css)调用会返回给定 CSS 选择器的第一个元素。elem.matches(css)不会查找任何内容,它只会检查elem是否与给定的 CSS 选择器匹配。它返回true或false。elem.closest(css)方法会查找与 CSS 选择器匹配的最近的祖先。
4.1.3 DOM的特性和属性(attribute,property)
-
特性(attribute)—— 写在 HTML 中的内容。
-
属性(property)—— DOM 对象中的内容。
-
elem.hasAttribute(name)—— 检查是否存在这个特性。 -
elem.getAttribute(name)—— 获取这个特性值。 -
elem.setAttribute(name, value)—— 设置这个特性值。 -
elem.removeAttribute(name)—— 移除这个特性。 -
elem.attributes—— 所有特性的集合。
在大多数情况下,最好使用 DOM 属性。仅当 DOM 属性无法满足开发需求,并且我们真的需要特性时,才使用特性。
4.1.4 修改DOM
创建节点
创建元素节点:document.createElement(tag)
创建文本节点:document.createTextNode(text)
插入节点
node.append(...nodes or strings)—— 在node末尾 插入节点或字符串,node.prepend(...nodes or strings)—— 在node开头 插入节点或字符串,node.before(...nodes or strings)—— 在node前面 插入节点或字符串,node.after(...nodes or strings)—— 在node后面 插入节点或字符串,node.replaceWith(...nodes or strings)—— 将node替换为给定的节点或字符串。
节点移除
想要移除一个节点,可以使用 node.remove()。
克隆节点
调用 elem.cloneNode(true) 来创建元素的一个“深”克隆 —— 具有所有特性(attribute)和子元素。如果我们调用 elem.cloneNode(false),那克隆就不包括子元素。
DocumentFragment
DocumentFragment 是一个特殊的 DOM 节点,用作来传递节点列表的包装器(wrapper)。
我们可以向其附加其他节点,但是当我们将其插入某个位置时,则会插入其内容。
4.2 浏览器事件
4.2.1 基本介绍
- HTML 特性
<input value="Click me" onclick="alert('Click!')" type="button">
- DOM 属性
elem.onclick = function() { alert('Thank you'); };
- addEventListener
element.addEventListener(event, handler[, options]);
// 移除绑定事件
element.removeEventListener(event, handler[, options]);
event:事件名,例如 "click"
handler:事件处理的回调函数,handler也可以是一个对象,会自动调用这个对象的handleEvent方法。
options:once|capture|passive
- 事件对象,回调函数默认接受一个参数
eventevent.type:获取事件类型,如"click"event.currentTarget:处理事件的 DOM 元素event.clientX | clientY:鼠标指针相对窗口的坐标event.target引发事件的层级最深的元素。event.eventPhase当前阶段(capturing=1,target=2,bubbling=3)。
4.2.2 事件冒泡与捕获
冒泡(bubbling)当一个事件发生在一个元素上,它会首先运行在该元素上的处理程序,然后运行其父元素上的处理程序,然后一直向上到其他祖先上的处理程序。
比如我们这里有3层嵌套,且各自拥有一个处理程序
<form onclick="alert('form')">FORM
<div onclick="alert('div')">DIV
<p onclick="alert('p')">P</p>
</div>
</form>
点击内部的 <p> 会首先运行 onclick:
- 在该
<p>上的。 - 然后是外部
<div>上的。 - 然后是外部
<form>上的。 - 以此类推,直到最后的
document对象。 停止冒泡
event.stopPropagation()阻止此事件向上冒泡,其它事件不影响event.stopImmediatePropagation()停止所有程序向上冒泡
捕获(capturing) DOM 事件标准描述了事件传播的 3 个阶段:
- 捕获阶段(Capturing phase)—— 事件(从 Window)向下走近元素。
- 目标阶段(Target phase)—— 事件到达目标元素。
- 冒泡阶段(Bubbling phase)—— 事件从元素上开始冒泡。
点击 <td>,事件首先通过祖先链向下到达元素(捕获阶段),然后到达目标(目标阶段),最后上升(冒泡阶段),在途中调用处理程序。
我们可以指定事件处理程序是在捕获阶段处理还是冒泡阶段处理
elem.addEventListener(..., {capture: true})
//如果为 `false`(默认值),则在冒泡阶段设置处理程序。
//如果为 `true`,则在捕获阶段设置处理程序。
4.2.3 事件委托
如果我们有许多以类似方式处理的元素,那么就不必为每个元素分配一个处理程序 —— 而是将单个处理程序放在它们的共同祖先上。
我们获取 event.target 以查看事件实际发生的位置并进行处理。
🚀面试题:时间委托的处理流程,及其优缺点。
利用事件委托的处理流程
- 在容器(container)上放一个处理程序。
- 在处理程序中 —— 检查源元素
event.target。 - 如果事件发生在我们感兴趣的元素内,那么处理该事件。
优点:
- 简化初始化并节省内存:无需添加许多处理程序。
- 更少的代码:添加或移除元素时,无需添加/移除处理程序。
- DOM 修改 :我们可以使用
innerHTML等,来批量添加/移除元素。
局限性:
- 首先,事件必须冒泡。而有些事件不会冒泡。此外,低级别的处理程序不应该使用
event.stopPropagation()。 - 其次,委托可能会增加 CPU 负载,因为容器级别的处理程序会对容器中任意位置的事件做出反应,而不管我们是否对该事件感兴趣。但是,通常负载可以忽略不计,所以我们不考虑它。
4.2.4 浏览器的默认行为
许多事件会自动触发浏览器执行某些行为。
- 点击一个链接 —— 触发导航(navigation)到该 URL。
- 点击表单的提交按钮 —— 触发提交到服务器的行为。
- 在文本上按下鼠标按钮并移动 —— 选中文本。
addEventListener 的 passive: true 选项告诉浏览器该行为不会被阻止。
阻止浏览器的默认行为
event.preventDefault()阻止浏览器的默认行为return false。只适用于通过on<event>分配的处理程序。
4.2.5 创建自定义事件
我们不仅可以分配事件处理程序,还可以从 JavaScript 生成事件。自定义事件可用于创建“图形组件”。
构造默认事件
let event = new Event(type[, options]);
构造自定义事件
let event = new CustomEvent(type[, options]);
bubbles: true/false—— 如果为true,那么事件会冒泡。cancelable: true/false—— 如果为true,那么“默认行为”就会被阻止。
默认情况下,以上两者都为 false:{bubbles: false, cancelable: false}。
dispatchEvent
事件对象被创建后,我们应该使用 elem.dispatchEvent(event) 调用在元素上“运行”它。
对于来自真实用户操作的事件,event.isTrusted 属性为 true,对于脚本生成的事件,event.isTrusted 属性为 false。
自定义事件
// 事件附带给处理程序的其他详细信息
elem.addEventListener("hello", function(event) {
alert(*event.detail.name*);
});
elem.dispatchEvent(new CustomEvent("hello", {
detail: { name: "John" }
}));
CustomEvent 提供了特殊的 detail 字段,以避免与其他事件属性的冲突。