我对JavaScript的一些认识

本文很多内容皆是来自阮一峰大神JavaScript标准参考教程,本人只是在根据此书脉络梳理自身对js知识掌握的同时对某些内容做总结记录......

1. 基本概念

JavaScript是一种轻量级的脚本语言,它本身不提供任何与I/O(输入input/输出output)相关的API,都要靠宿主环境(host)提供,去调用宿主环境提供的API。

所谓“脚本语言”,是指其本身不具备开发操作系统的能力,而是只用来编写控制其他大型应用程序的“脚本”。

JavaScript 的发明目的,就是作为浏览器的内置脚本语言,为网页开发者提供操控浏览器的能力。它是目前唯一一种通用的浏览器脚本语言。

2. 语法

语句和表达式的区别在于,前者主要为了进行某种操作,一般情况下不需要返回值;后者则是为了得到返回值,一定会返回一个值。

语句以分号结尾,一个分号就表示一个语句结束。

分号前面可以没有任何内容,JavaScript引擎将其视为空语句。

如果使用var重复声明一个已经存在的变量,第二次声明是无效的。

但是,如果第二次声明的时候还进行了赋值,则会覆盖掉前面的值。

break语句和continue语句

break语句用于跳出代码块或循环。

continue语句用于立即终止本轮循环,返回循环结构的头部,开始下一轮循环。

3. 原型原型链

面向对象编程很重要的一个方面,就是对象的继承。A 对象通过继承 B 对象,就能直接拥有 B 对象的所有属性和方法。这对于代码的复用是非常有用的。

大部分面向对象的编程语言,都是通过“类”(class)来实现对象的继承。JavaScript 语言的继承则是通过“原型对象”(prototype)。

JavaScript 继承机制的设计思想就是,原型对象的所有属性和方法,都能被实例对象共享。也就是说,如果属性和方法定义在原型上,那么所有实例对象就能共享,不仅节省了内存,还体现了实例对象之间的联系。

“原型链”(prototype chain):对象到原型,再到原型的原型…… 这就形成了原型链。

如果一层层地上溯,所有对象的原型最终都可以上溯到Object.prototype,即Object构造函数的prototype属性。也就是说,所有对象都继承了Object.prototype的属性。这就是所有对象都有valueOftoString方法的原因,因为这是从Object.prototype继承的。

Object.prototype对象有没有它的原型呢?回答是Object.prototype的原型是nullnull没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是null。原型链到此为止。

读取对象的某个属性时,JavaScript 引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的Object.prototype还是找不到,则返回undefined

注意,一级级向上,在整个原型链上寻找某个属性,对性能是有影响的。所寻找的属性在越上层的原型对象,对性能的影响越大。如果寻找某个不存在的属性,将会遍历整个原型链。

instanceof运算符返回一个布尔值,表示对象是否为某个构造函数的实例。

var v = new Vehicle();
v instanceof Vehicle // true  对象v是构造函数Vehicle的实例,所以返回true

4. promise

Promise 的设计思想是,所有异步任务都返回一个 Promise 实例。Promise 实例有一个then方法,用来指定下一步的回调函数。

function f1(resolve, reject) {
  // 异步代码...
}

var p1 = new Promise(f1);
p1.then(f2);

5. DOM对象

document.scrollingElement属性返回文档的滚动元素。也就是说,当文档整体滚动时,到底是哪个元素在滚动。

标准模式下,这个属性返回的文档的根元素document.documentElement(即<html>)。兼容(quirk)模式下,返回的是<body>元素,如果该元素不存在,返回null

// 页面滚动到浏览器顶部
document.scrollingElement.scrollTop = 0;

document.fullscreenElement属性返回当前以全屏状态展示的 DOM 元素。如果不是全屏状态,该属性返回null

我们可以通过document.fullscreenElement可以知道<video>元素有没有处在全屏状态,从而判断用户行为。

if (document.fullscreenElement.nodeName == 'VIDEO') {
  console.log('全屏播放视频');
}

document.documentURI属性和document.URL属性都返回一个字符串,表示当前文档的网址。

不同之处是它们继承自不同的接口,documentURI继承自Document接口,可用于所有文档;URL继承自HTMLDocument接口,只能用于 HTML 文档。

document.domain属性返回当前文档的域名,不包含协议和接口。

Location对象是浏览器提供的原生对象,提供 URL 相关的信息和操作方法。通过window.locationdocument.location属性,可以拿到这个对象。

div1节点,插入一个值为newValmy_attrib属性

document.getElementById('div1').setAttribute('my_attrib', 'newVal');

6. 事件

Element.matches方法返回一个布尔值,表示当前元素是否匹配给定的 CSS 选择器。

if (el.matches('.someClass')) {
  console.log('Match!');
}

Element.addEventListener():添加事件的回调函数

document.getElementById('box').addEventListener('click',function(){
  console.log('输出')
});

Element.removeEventListener():移除事件监听函数

注意,removeEventListener方法移除的监听函数,必须是addEventListener方法添加的那个监听函数,而且必须在同一个元素节点,否则无效。

function Cons(){
    console.log('输出')
}
document.getElementById('box').addEventListener('click',Cons,false);
document.getElementById('box').removeEventListener('click',Cons,false);

Element.dispatchEvent():触发事件

document.querySelector('#boxes').addEventListener('click',function(){//在boxes的点击事件中触发box的点击事件
  var event = new Event('click');
  document.getElementById('box').dispatchEvent(event);
})

Element.scrollIntoView方法滚动当前元素,进入浏览器的可见区域;

该方法可以接受一个布尔值作为参数。如果为true,表示元素的顶部与当前区域的可见部分的顶部对齐(前提是当前区域可滚动);

如果为false,表示元素的底部与当前区域的可见部分的尾部对齐(前提是当前区域可滚动)。如果没有提供该参数,默认为true

document.getElementById('six').addEventListener('click',function(){
     document.getElementById('six').scrollIntoView();
      // document.getElementById('six').scrollIntoView(false);
})

7. 重流重绘

渲染树转换为网页布局,称为“布局流”(flow);布局显示到页面的这个过程,称为“绘制”(paint)。它们都具有阻塞效应,并且会耗费很多时间和计算资源。

页面生成以后,脚本操作和样式表操作,都会触发“重流”(reflow)和“重绘”(repaint)。用户的互动也会触发重流和重绘,比如设置了鼠标悬停(a:hover)效果、页面滚动、在输入框中输入文本、改变窗口大小等等。

重流和重绘并不一定一起发生,重流必然导致重绘,重绘不一定需要重流。比如改变元素颜色,只会导致重绘,而不会导致重流;改变元素的布局,则会导致重绘和重流。

大多数情况下,浏览器会智能判断,将重流和重绘只限制到相关的子树上面,最小化所耗费的代价,而不会全局重新生成网页。

作为开发者,应该尽量设法降低重绘的次数和成本。比如,尽量不要变动高层的 DOM 元素,而以底层 DOM 元素的变动代替;再比如,重绘table布局和flex布局,开销都会比较大。

重流重绘优化技巧:

读取 DOM 或者写入 DOM,尽量写在一起,不要混杂。不要读取一个 DOM 节点,然后立刻写入,接着再读取一个 DOM 节点。

缓存 DOM 信息。

不要一项一项地改变样式,而是使用 CSS class 一次性改变样式。

使用documentFragment操作 DOM

动画使用absolute定位或fixed定位,这样可以减少对其他元素的影响。

只在必要时才显示隐藏元素。

使用window.requestAnimationFrame(),因为它可以把代码推迟到下一次重流时执行,而不是立即要求页面重流。

使用虚拟DOM(virtual DOM)库。

//下面是一个window.requestAnimationFrame()对比效果的例子
// 重绘代价高
function doubleHeight(element) {
  var currentHeight = element.clientHeight;
  element.style.height = (currentHeight * 2) + 'px';
}

all_my_elements.forEach(doubleHeight);

// 重绘代价低
function doubleHeight(element) {
  var currentHeight = element.clientHeight;

  window.requestAnimationFrame(function () {
    element.style.height = (currentHeight * 2) + 'px';
  });
}

all_my_elements.forEach(doubleHeight);

8. window对象

window.top属性指向最顶层窗口,主要用于在子窗口里面获取顶层的父窗口。

window.self:当前窗口,即自身。

window.parent属性指向父窗口。如果当前窗口没有父窗口,window.parent指向自身。

if (window.parent !== window.top) {
  // 表明当前窗口嵌入不止一层
}
if (window.top === window.self) {
  // 当前窗口是顶层窗口
} else {
  // 当前窗口是子窗口
}

让父窗口的访问历史后退一次

window.parent.history.back();

window.open方法用于新建另一个浏览器窗口,类似于浏览器菜单的新建窗口选项。它会返回新窗口的引用,如果无法新建窗口,则返回null

window.open(url, windowName, [windowFeatures])

open方法一共可以接受三个参数。

url:字符串,表示新窗口的网址。如果省略,默认网址就是about:blank

windowName:字符串,表示新窗口的名字。如果该名字的窗口已经存在,则占用该窗口,不再新建窗口。如果省略,就默认使用_blank,表示新建一个没有名字的窗口。

windowFeatures:字符串,内容为逗号分隔的键值对(详见下文),表示新窗口的参数,比如有没有提示栏、工具条等等。如果省略,则默认打开一个完整 UI 的新窗口。如果新建的是一个已经存在的窗口,则该参数不起作用,浏览器沿用以前窗口的参数。

var popup = window.open(
  'somepage.html',
  'DefinitionsWindows',
  'height=200,width=200,location=no,status=yes,resizable=yes,scrollbars=yes'
);
//表示,打开的新窗口高度和宽度都为200像素,没有地址栏和滚动条,但有状态栏,允许用户调整大小。

第三个属性可以设置如下属性:

  • left:新窗口距离屏幕最左边的距离(单位像素)。注意,新窗口必须是可见的,不能设置在屏幕以外的位置。
  • top:新窗口距离屏幕最顶部的距离(单位像素)。
  • height:新窗口内容区域的高度(单位像素),不得小于100。
  • width:新窗口内容区域的宽度(单位像素),不得小于100。
  • outerHeight:整个浏览器窗口的高度(单位像素),不得小于100。
  • outerWidth:整个浏览器窗口的宽度(单位像素),不得小于100。
  • menubar:是否显示菜单栏。
  • toolbar:是否显示工具栏。
  • location:是否显示地址栏。
  • personalbar:是否显示用户自己安装的工具栏。
  • status:是否显示状态栏。
  • dependent:是否依赖父窗口。如果依赖,那么父窗口最小化,该窗口也最小化;父窗口关闭,该窗口也关闭。
  • minimizable:是否有最小化按钮,前提是dialog=yes
  • noopener:新窗口将与父窗口切断联系,即新窗口的window.opener属性返回null,父窗口的window.open()方法也返回null
  • resizable:新窗口是否可以调节大小。
  • scrollbars:是否允许新窗口出现滚动条。
  • dialog:新窗口标题栏是否出现最大化、最小化、恢复原始大小的控件。
  • titlebar:新窗口是否显示标题栏。
  • alwaysRaised:是否显示在所有窗口的顶部。
  • alwaysLowered:是否显示在父窗口的底下。
  • close:新窗口是否显示关闭按钮。

window.close方法用于关闭当前窗口,一般只用来关闭window.open方法新建的窗口。该方法只对顶层窗口有效,iframe框架之中的窗口使用该方法无效。

popup.close()

浏览器脚本发生错误时,会触发window对象的error事件。我们可以通过window.onerror属性对该事件指定回调函数。

window.onerror = function (message, filename, lineno, colno, error) {
  console.log("出错了!--> %s", error.stack);
};

9. screen对象

Screen.orientation:返回一个对象,表示屏幕的方向。

该对象的type属性是一个字符串,表示屏幕的具体方向,landscape-primary表示横放,landscape-secondary表示颠倒的横放,portrait-primary表示竖放,portrait-secondary

我们可以根据屏幕的宽度,将用户导向不同网页的代码。

if ((screen.width <= 800) && (screen.height <= 600)) {
  window.location.replace('small.html');
} else {
  window.location.replace('wide.html');
}

10. navigator对象

navigator.userAgent属性返回浏览器的 User Agent 字符串,表示浏览器的厂商和版本信息。

通过userAgent可以大致识别所有移动设备的浏览器

var ua = navigator.userAgent.toLowerCase();

if (/mobi|android|touch|mini/i.test(ua)) {
  // 移动设备的浏览器
} else {
  // 非移动设备浏览器
}

Navigator.platform属性返回用户的操作系统信息,比如MacIntelWin32Linux x86_64等 。

navigator.onLine属性返回一个布尔值,表示用户当前在线还是离线(浏览器断线)。

有时,浏览器可以连接局域网,但是局域网不能连通外网。这时,有的浏览器的onLine属性会返回true,所以不能假定只要是true,用户就一定能访问互联网。不过,如果是false,可以断定用户一定离线。

用户变成在线会触发online事件,变成离线会触发offline事件,可以通过window.ononlinewindow.onoffline指定这两个事件的回调函数。

window.addEventListener('offline', function(e) { console.log('offline'); });
window.addEventListener('online', function(e) { console.log('online'); });

11. URL的解码和编码

网页的 URL 只能包含合法的字符,这可以分成两类。

  • URL 元字符:分号(;),逗号(’,’),斜杠(/),问号(?),冒号(:),at(@),&,等号(=),加号(+),美元符号($),井号(#
  • 语义字符:a-zA-Z0-9,连词号(-),下划线(_),点(.),感叹号(!),波浪线(~),星号(*),单引号(\),圆括号(()`)

除了以上字符,其他字符出现在 URL 之中都必须转义,规则是根据操作系统的默认编码,将每个字节转为百分号(%)加上两个大写的十六进制字母。比如,UTF-8 的操作系统上,http://www.example.com/q=春节这个 URL 之中,汉字“春节”不是 URL 的合法字符,所以被浏览器自动转成http://www.example.com/q=%E6%98%A5%E8%8A%82。其中,“春”转成了%E6%98%A5,“节”转成了“%E8%8A%82”。这是因为“春”和”节“的 UTF-8 编码分别是E6 98 A5E8 8A 82,将每个字节前面加上百分号,就构成了 URL 编码。

encodeURI()方法用于转码整个 URL。它的参数是一个字符串,代表整个 URL。它会将元字符和语义字符之外的字符,都进行转义。

encodeURIComponent()方法用于转码 URL 的组成部分,会转码除了语义字符之外的所有字符,即元字符也会被转码。所以,它不能用于转码整个 URL。它接受一个参数,就是 URL 的片段。

encodeURI('http://www.example.com/q=春节')
// "http://www.example.com/q=%E6%98%A5%E8%8A%82"
encodeURIComponent('春节')
// "%E6%98%A5%E8%8A%82"
encodeURIComponent('http://www.example.com/q=春节')
// "http%3A%2F%2Fwww.example.com%2Fq%3D%E6%98%A5%E8%8A%82"
//encodeURIComponent()会连 URL 元字符一起转义,所以如果转码整个 URL 就会出错。

decodeURI()方法用于整个 URL 的解码。它是encodeURI()方法的逆运算。它接受一个参数,就是转码后的 URL。

decodeURIComponent()用于URL 片段的解码。它是encodeURIComponent()方法的逆运算。它接受一个参数,就是转码后的 URL 片段。

12. storage存储对象

注意,Storage.setItem()两个参数都是字符串。如果不是字符串,会自动转成字符串,再存入浏览器。

写入不一定要用setItem(),直接赋值也是可以的。

// 下面三种写法等价
window.localStorage.foo = '123';
window.localStorage['foo'] = '123';
window.localStorage.setItem('foo', '123');

Storage 接口储存的数据发生变化时,会触发 storage 事件,可以指定这个事件的监听函数。

function onStorageChange(e) {
  console.log(e.key);
}

window.addEventListener('storage', onStorageChange);

注意,该事件有一个很特别的地方,就是它不在导致数据变化的当前页面触发,而是在同一个域名的其他窗口触发。

也就是说,如果浏览器只打开一个窗口,可能观察不到这个事件。比如同时打开多个窗口,当其中的一个窗口导致储存的数据发生改变时,只有在其他窗口才能观察到监听函数的执行。

可以通过这种机制,实现多个窗口之间的通信。