我对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
的属性。这就是所有对象都有valueOf
和toString
方法的原因,因为这是从Object.prototype
继承的。
Object.prototype
对象有没有它的原型呢?回答是Object.prototype
的原型是null
。null
没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是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.location
和document.location
属性,可以拿到这个对象。
为div1
节点,插入一个值为newVal
的my_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
属性返回用户的操作系统信息,比如MacIntel
、Win32
、Linux x86_64
等 。
navigator.onLine
属性返回一个布尔值,表示用户当前在线还是离线(浏览器断线)。
有时,浏览器可以连接局域网,但是局域网不能连通外网。这时,有的浏览器的onLine
属性会返回true
,所以不能假定只要是true
,用户就一定能访问互联网。不过,如果是false
,可以断定用户一定离线。
用户变成在线会触发online
事件,变成离线会触发offline
事件,可以通过window.ononline
和window.onoffline
指定这两个事件的回调函数。
window.addEventListener('offline', function(e) { console.log('offline'); }); window.addEventListener('online', function(e) { console.log('online'); });
11. URL的解码和编码
网页的 URL 只能包含合法的字符,这可以分成两类。
- URL 元字符:分号(
;
),逗号(’,’),斜杠(/
),问号(?
),冒号(:
),at(@
),&
,等号(=
),加号(+
),美元符号($
),井号(#
) - 语义字符:
a-z
,A-Z
,0-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 A5
和E8 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);
注意,该事件有一个很特别的地方,就是它不在导致数据变化的当前页面触发,而是在同一个域名的其他窗口触发。
也就是说,如果浏览器只打开一个窗口,可能观察不到这个事件。比如同时打开多个窗口,当其中的一个窗口导致储存的数据发生改变时,只有在其他窗口才能观察到监听函数的执行。
可以通过这种机制,实现多个窗口之间的通信。