跳到主要内容

JavaScript 基础

JavaScript 基础知识

JavaScript = ECMAScript + 宿主环境提供的 API。

返回上级

用一张图来表示各种模块规范语法和它们所处环境之间的关系:

1853-6m4EOG

每个 JS 的运行环境都有一个解析器,否则这个环境也不会认识 JS 语法。它的作用就是用 ECMAScript 的规范去解释 JS 语法,也就是处理和执行语言本身的内容,例如按照逻辑正确执行 var a = "123"; function func() {console.log("hahaha");} 之类的内容。

在解析器的上层,每个运行环境都会在解释器的基础上封装一些环境相关的 API。例如 Node.js 中的 global 对象、process 对象,浏览器中的 window 对象、document 对象等等。这些运行环境的 API 受到各自规范的影响,例如浏览器端的 W3C 规范,它们规定了 window 对象和 document 对象上的 API 内容,以使得我们能让 document.getElementById 这样的 API 在所有浏览器上运行正常。

  1. 类浏览器(web 应用)的 JavaScript 包括:

    1. ECMAScript:描述该语言的语法和基本对象。
    2. Web API:
      1. 文档对象模型(DOM):描述处理网页内容的方法和接口。
      2. 浏览器对象模型(BOM):描述与浏览器进行交互的方法和接口。
  2. Node.js 的 JavaScript 包括:

    1. ECMAScript:描述该语言的语法和基本对象。
    2. 操作系统的 API:
      1. 操作系统(OS)
      2. 文件系统(file)
      3. 网络系统(net)
      4. 数据库(database)

零星总结

session 共享域

通过点击链接(或者用了 window.open)打开的新标签页(同域)之间是属于同一个 session 的,但新开一个标签页总是会初始化一个新的 session,即使网站是一样的,它们也不属于同一个 session

PS:在 a 页面通过window.open()打开同域地址得到的 b 页面,b 页面会有 a 页面当前 sessionStorage 的一份独立拷贝,这两个 sessionStorage 互不影响。

重绘(repaint)和重排(或叫回流,reflow)的区别和关系

  • 重绘:DOM 变化不影响元素的几何属性时产生重绘。当渲染树中的元素外观(如:颜色)发生改变,不影响布局时,产生重绘
  • 重排:浏览器需要重新计算元素的几何属性,而且其他元素的几何属性或位置可能也会因此改变受到影响。当渲染树中的元素布局(如:尺寸、位置、隐藏/状态状态)发生改变时,产生重排
  • 注意:JS 获取 Layout 属性值(如:offsetLeft、scrollTop、getComputedStyle 等)也会引起重排。因为浏览器需要通过重排计算最新值
  • 重绘顾名思义重新绘制,重排顾名思义重新排版。重排必将引起重绘,而重绘不一定会引起重排

PS:Layout 指布局

如何最小化重绘和重排

  • 需要要对元素进行复杂的操作时,可以先隐藏(display:"none"),操作完成后再显示。
  • 需要创建多个 DOM 节点时,使用 DocumentFragment 创建完后一次性的加入 document。
  • 缓存 Layout 属性值,如:var left = elem.offsetLeft; 这样,多次使用 left 只产生一次重排。
  • 尽量避免用 table 布局(table 元素一旦触发重排就会导致 table 里所有的其它元素重排)。
  • 避免使用 css 表达式(expression),因为每次调用都会重新计算值(包括加载页面)。
  • 尽量使用 css 属性简写,如:用 border 代替 border-width, border-style, border-color。
  • 批量修改元素样式:elem.className 和 elem.style.cssText 代替 elem.style.xxx(走 css 方式)。

关键字:缓存,减少次数

this 对象

  • this 总是指向函数的直接调用者
  • 如果有 new 关键字,this 指向 new 出来的实例对象
  • 在事件中,this 指向触发这个事件的对象

什么是 Window 和 Document 对象

  • Window 对象表示当前浏览器的窗口,是 JavaScript 的顶级对象。
  • 我们创建的所有对象、函数、变量都是 Window 对象的成员。
  • Window 对象的方法和属性是在全局范围内有效的。
  • Document 对象是 HTML 文档的根节点与所有其他节点(元素节点,文本节点,属性节点, 注释节点)
  • Document 对象使我们可以通过脚本对 HTML 页面中的所有元素进行访问
  • Document 对象是 Window 对象的一部分,可通过 window.document 属性对其进行访问

DOM 的发展

  • DOM:文档对象模型(Document Object Model),定义了访问 HTML 和 XML 文档的标准,与编程语言及平台无关
  • DOM0:提供了查询和操作 Web 文档的内容 API。未形成标准,实现混乱。
  • DOM1:W3C 提出标准化的 DOM,简化了对文档中任意部分的访问和操作。如:JavaScript 中的 Document 对象
  • DOM2:原来 DOM 基础上扩充了鼠标事件等细分模块,增加了对 CSS 的支持。如:getComputedStyle(elem, pseudo)
  • DOM3:增加了 XPath 模块和加载与保存(Load and Save)模块。如:XPathEvaluator

DOM0,DOM2,DOM3 事件处理方式区别

DOM1 没事件

  • DOM0 级事件处理方式:
    • btn.onclick = func;
    • btn.onclick = null;
  • DOM2 级事件处理方式:
    • btn.addEventListener('click', func, false);
    • btn.removeEventListener('click', func, false);
  • DOM3 级事件处理方式(扩展 DOM2 方法):
    • eventUtil.addListener(input, "textInput", func);
    • eventUtil 是自定义对象,textInput 是 DOM3 级事件

事件“捕获”和“冒泡”执行顺序和事件的执行次数

  • 按照 W3C 标准的事件:首是进入捕获阶段,直到达到目标元素,再进入冒泡阶段
  • 事件执行次数(DOM2-addEventListener):取决于元素上绑定事件的个数
    • 注意 1:前提是事件确实被触发
    • 注意 2:事件绑定几次就算几个事件,即使类型和功能完全一样也不会“覆盖”
  • 事件执行顺序:判断的关键是否目标元素
    • 非目标元素:根据 W3C 的标准执行:捕获->目标元素->冒泡(不依据事件绑定顺序)
    • 目标元素:依据事件绑定顺序:先绑定的事件先执行(不依据捕获冒泡标准)
    • 最终顺序:父元素捕获->目标元素事件 1->目标元素事件 2->子元素捕获->子元素冒泡->父元素冒泡
    • 注意:子元素事件执行前提是事件确实“落”到子元素布局区域上,而不是简单的具有嵌套关系

在一个 DOM 上同时绑定两个点击事件:一个用捕获,一个用冒泡。事件会执行几次,先执行冒泡还是捕获:

  • 该 DOM 上的事件如果被触发,会执行两次(执行次数等于绑定次数)
  • 如果该 DOM 是目标元素,则按事件绑定顺序执行,不区分冒泡/捕获
  • 如果该 DOM 是处于事件流中的非目标元素,则先执行捕获,后执行冒泡

事件的代理/委托

事件委托是指将事件绑定到目标元素的父元素上,利用冒泡机制触发该事件

  • 优点:
    • 可以减少事件注册,节省大量内存占用
    • 可以将事件应用于动态添加的子元素上
  • 缺点: 使用不当会造成事件在不应该触发时触发

示例:

ulElement.addEventListener(
"click",
function (e) {
var target = event.target || event.srcElement;
if (!!target && target.nodeName.toUpperCase() === "LI") {
console.log(target.innerHTML);
}
},
false
);

W3C 事件的 target 与 currentTarget 的区别

  • target 只会出现在事件流的目标阶段
  • currentTarget 可能出现在事件流的任何阶段
  • 当事件流处在目标阶段时,二者的指向相同
  • 当事件流处于捕获或冒泡阶段时:currentTarget 指向当前事件活动的对象(一般为父级)

dispatchEvent 事件广播

  • W3C: 使用 dispatchEvent 方法
var dispatchEvent = function (element, event) {
var mockEvent = document.createEvent("HTMLEvents");
mockEvent.initEvent(event, true, true);
return !element.dispatchEvent(mockEvent);
};

鼠标事件中客户区坐标、页面坐标、屏幕坐标

  • 客户区坐标:鼠标指针在可视区中的水平坐标(clientX)和垂直坐标(clientY)
  • 页面坐标:鼠标指针在页面布局中的水平坐标(pageX)和垂直坐标(pageY)
  • 屏幕坐标:设备物理屏幕的水平坐标(screenX)和垂直坐标(screenY)

1042-eikxCs

nullundefined 的区别

  • undefined 表示不存在这个值。

  • undefined 是一个表示"无"的原始值或者说表示"缺少值",就是此处应该有一个值,但是还没有定义。当尝试读取时会返回 undefined,例如变量被声明了,但没有赋值时,就等于 undefined

  • null 表示一个对象被定义了,值为“空值”。

  • null 是一个对象(空对象, 没有任何属性和方法)。

  • 在验证 null 时,一定要使用 === ,因为 == 无法分别 nullundefined

    undefined is a property of the global object; i.e., it is a variable in global scope. The initial value of undefined is the primitive value undefined. In modern browsers (JavaScript 1.8.5 / Firefox 4+), undefined is a non-configurable, non-writable property per the ECMAScript 5 specification. Even when this is not the case, avoid overriding it.

eval 是什么

eval 究竟是在执行什么?

在代码eval(x)中,x 必须是一个字符串,不能是其他任何类型的值,也不能是一个字符串对象。如果尝试在 x 中传入其他的值,那么 eval() 将直接以该值为返回值。eval() 会按照 JavaScript 语法规则来尝试解析字符串 x,包括对一些特殊字面量(例如 8 进制)的语法解析。

// JavaScript 在源代码层面支持 8 进制
eval("012");
// 10

// 但 parseInt() 不支持 8 进制(除非显式指定 radix 参数)
parseInt("012");
// 12

eval() 会将参数 x 强制理解为语句行,这样一来,当按照“语句 -> 表达式”的顺序解析时,“”将被优先理解为语句中的大括号。于是,下面的代码就成了 JavaScript 初学者的经典噩梦:

// 试图返回一个对象
eval("{abc: 1}");
// 1

由于第一个字符被理解为块语句,那么“abc:”就将被解析成标签语句;接下来,"1"会成为一个“单值表达式语句”。所以,结果是返回了这个表达式的值,也就是 1。

eval 在哪儿执行

eval 总是将代码执行在当前上下文的“当前位置”。这里的所谓的“当前上下文”并不是它字面意思中的“代码文本上下文”,而是指“(与执行环境相关的)执行上下文”。JavaScript 的执行系统相关的两个组件:环境和上下文。

严格地来讲,环境是 JavaScript 在语言系统中的静态组件,而上下文是它在执行系统中的动态组件。

环境

JavaScript 中,环境可以细分为四种,并由两个类别的基础环境组件构成。这四种环境是:全局(Global)、函数(Function)、模块(Module)和 Eval 环境;两个基础组件的类别分别是:声明环境(Declarative Environment)和对象环境(Object Environment)。

**声明环境就是名字表,可以是引擎内核用任何方式来实现的一个“名字 -> 数据”的对照表;对象环境是 JavaScript 的一个对象,用来“模拟 / 映射”成上述的对照表的一个结果,你也可以把它看成一个具体的实现。**所以,

  • 概念:所有的“环境”本质上只有一个功能,就是用来管理“名字 -> 数据”的对照表;
  • 应用:“对象环境”只为全局环境的 global 对象,或with (obj)...语句中的对象 obj 创建,其他情况下创建的环境,都必然是“声明环境”。

所以,所谓四种环境,其实是上述的两种基础组件进一步应用的结果。其中,全局(Global)环境是一个复合环境,它由一对“对象环境 + 声明环境”组成;其他 3 种环境,都是一个单独的声明环境。

你需要关注到的一个事实是:所有的四种环境都与执行相关——看起来它们“像是”为每种可执行的东西都创建了一个环境,但是它们事实上都不是可以执行的东西,也不是执行系统(执行引擎)所理解的东西。更加准确地说:上述四种环境,本质上只是为 JavaScript 中的每一个“可以执行的语法块”创建了一个名字表的影射而已。

执行上下文

JavaScript 的执行系统由一个执行栈和一个执行队列构成的。在执行队列中保存的是待执行的任务,称为 Job。这是一个抽象概念,它指明在“创建”这个执行任务时的一些关联信息,以便正式“执行”时可以参考它;而“正式的执行”发生在将一个新的上下文被“推入(push)”执行栈的时候。

所以,上下文是一个任务“执行 / 不执行”的关键。如果一个任务只是任务,并没有执行,那么也就没有它的上下文;如果一个上下文从栈中撤出,那么就必须有地方能够保存这个上下文,否则可执行的信息就丢失了(这种情况并不常见);如果一个新上下文被“推入(push)”栈,那么旧的上下文就被挂起并压向栈底;如果当前活动上下文被“弹出(pop)”栈,那么处在栈底的旧上下文就被恢复了。

NOTE:很少需要在用户代码(在它的执行过程中)撤出和保存上下文的过程,但这的确存在。比如生成器(GeneratorContext),或者异步调用(AsyncContext)。

而每一个上下文只关心两个高度抽象的信息:其一是执行点(包括状态和位置),其二是执行时的参考,也就是前面一再说到的“名字的对照表”。

所以,重要的是:每一个执行上下文都需要关联到一个对照表。这个对照表,就称为“词法环境(Lexical Environment)”。显然,它可以是上述四种环境之任一;并且,更加重要的,也可是两种基础组件之任一!

如上是一般性质的执行引擎逻辑,对于大多数“通用的”执行环境来说,这是足够的。

但对于 JavaScript 来说这还不够,因为 JavaScript 的早期有一个“能够超越词法环境”的东西存在,就是“var 变量”。所谓词法环境,就是一个能够表示标识符在源代码(词法)中的位置的环境,由于源代码分块,所以词法环境就可以用“链式访问”来映射“块之间的层级关系”。但是“var 变量”突破了这个设计限制,例如:

var x = 1;
if (true) {
var x = 2;
with (new Object()) {
var x = 3;
}
}

这个示例中的“1、2、3”所在的“var 变量”x,都突破了它们所在的词法作用域(或对应的词法环境),而指向全局的 x。

于是,自 ECMAScript 5 开始约定,ECMAScript 的执行上下文将有两个环境,一个称为词法环境,另一个就称为变量环境(Variable Environment);所有传统风格的“var 声明和函数声明”将通过“变量环境”来管理。

小结

  • 它的功能是把对应的字符串解析成 JS 代码并运行
  • 应该避免使用 eval,一不安全,二非常耗性能(2 次,一次解析成 js 语句,一次执行)
  • 由 JSON 字符串转换为 JSON 对象的时候可以用 eval,var obj =eval('('+ str +')')

严格模式的限制

严格模式主要有以下限制:

  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用 with 语句
  • 不能对只读属性赋值,否则报错
  • 不能使用前缀 0 表示八进制数,否则报错
  • 不能删除不可删除的属性,否则报错
  • 不能删除变量 delete prop,会报错,只能删除属性 delete global[prop]
  • eval 不会在它的外层作用域引入变量
  • evalarguments 不能被重新赋值
  • arguments 不会自动反映函数参数的变化
  • 不能使用 arguments.callee
  • 不能使用 arguments.caller
  • 禁止 this 指向全局对象
  • 不能使用 fn.callerfn.arguments 获取函数调用的堆栈
  • 增加了保留字(比如 protectedstaticinterface

script 放在 </body> 之前后差异

<script>放在</body>之前和之后有什么区别,浏览器会如何解析它们?

  • 按照 HTML 标准,在</body>结束后出现<script>或任何元素的开始标签,都是解析错误
  • 虽然不符合 HTML 标准,但浏览器会自动容错,使实际效果与写在</body>之前没有区别
  • 浏览器的容错机制会忽略<script>之前的</body>,视作<script>仍在 body 体内。

JS 伪协议

伪协议(自定义协议):操作系统提供支持的、为关联应用程序而使用的、在标准协议(httphttpsftp等)之外的,一种协议(mailtotelfiledata自定义URL Scheme等)。

  1. 由 JS 解释器运行,若最后一个执行结果(;分割执行语句)是String类型,则返回给当前页面替换原页面内容(允许任何 HTML 标签)
  2. 所有直接修改 URL 的地方都可使用,如:<a><iframe><img>src属性,window.location.hrefwindow.open
  3. 为了 JS 与 HTML 解耦合,尽量不要使用其参与 JS 逻辑

JS 的预编译

  1. JS 是一门脚本语言,不经过编译而直接运行,但运行前先进行预编译。

  2. JS 的预编译是以代码块<script></script>为范围,即每遇到一个代码块都会进行:预编译 -> 执行

  3. 预编译:在内存中开辟一块空间,用来存放变量函数

    为使用var声明的变量、使用function声明的函数在内存中开辟一块空间,用来存放两者声明(不会赋值,所有变量的值都是undefined、函数内容会被预编译);

    const/let不允许同名声明。

    • 在预编译时,function的优先级比var/let/const高:

      1. 在预编译阶段,同时声明同一名称的函数和变量(顺序不限),会被声明为函数。
      2. 在执行阶段,若变量有赋值,则这个名称会重新赋值给变量。

      示例:

      // 预编译阶段a1/b2为函数,运行时a1/b2赋值成为变量
      console.log(a1); // => ƒ a1() {}
      var a1 = 1;
      function a1() {}
      console.log(a1); // => 1

      console.log(b2); // => ƒ b2() {}
      function b2() {}
      var b2 = 1;
      console.log(b2); // => 1

      // 预编译阶段c3/d4为函数,运行时没有赋值
      console.log(c3); // => ƒ c3() {}
      var c3;
      function c3() {}
      console.log(c3); // => ƒ c3() {}

      console.log(d4); // => ƒ c4() {}
      function d4() {}
      var d4;
      console.log(d4); // => ƒ c4() {}

      // 预编译阶段e5为变量,运行时被赋值给匿名函数
      console.log(e5); // => undefined
      var e5 = function () {};
      console.log(e5); // => ƒ () {}(匿名函数)
  4. 变量赋值是在 JS 执行阶段(运行时)进行的。