第三篇主要讲原生js的方法
Mutation Observer APi用来监听DOM变动。DOM的任何变动,比如节点的增减、属性的变动、文本的变动,这个api都可以得到通知
DOM变动就会触发Mutation Observer,与事件的区别是事件是同步触发,也就是DOM的变动立刻会触发相应的事件,Mutation Observer是异步触发,需要等当前所有的DOM操作都结束才会触发
Mutation Observer有以下特点:
实例方法
observe()
方法用来监听,接受两个参数:所要观察的DOM节点,以及配置对象,也就是观察的特定变动
disconnect()
方法用来停止观察,调用该方法后DOM再变动也不会触发
takeRecords()
方法用来
实例属性
document.body,document.head:document.body
属性指向<body>
节点,document.head
属性指向<head>
节点。这两个属性总是存在的,如果网页源码里面省略了<head>
或<body>
,浏览器会自动创建。另外,这两个属性是可写的,如果改写它们的值,相当于移除所有子节点。
document.activeElement:document.activeElement
属性返回获得当前焦点(focus)的 DOM 元素。通常,这个属性返回的是<input>
、<textarea>
、<select>
等表单元素,如果当前没有焦点元素,返回<body>
元素或null
document.fullscreenElement:document.fullscreenElement
属性返回当前以全屏状态展示的 DOM 元素。如果不是全屏状态,该属性返回null
document.hidden
属性返回一个布尔值,表示当前页面是否可见。如果窗口最小化、浏览器切换了 Tab,都会导致导致页面不可见,使得document.hidden
返回true
.这个属性是 Page Visibility API 引入的,一般都是配合这个 API 使用
document.visibilityState
返回文档的可见状态。它的值有四种
visible
:页面可见。注意,页面可能是部分可见,即不是焦点窗口,前面被其他窗口部分挡住了。hidden
:页面不可见,有可能窗口最小化,或者浏览器切换到了另一个 Tab。prerender
:页面处于正在渲染状态,对于用户来说,该页面不可见。unloaded
:页面从内存里面卸载了。document.readyState
属性返回当前文档的状态,共有三种可能的值。
loading
:加载 HTML 代码阶段(尚未完成解析)interactive
:加载外部资源阶段complete
:加载完成实例方法
document.open()
方法清除当前文档内的所有内容,使得文档处于可写状态
document.close()
方法关闭document.open打开的文档
document.elementFromPoint()
方法返回位于页面指定位置最上层的元素节点
document.write
方法用于向当前文档写入内容。在网页的首次渲染阶段,只要页面没有关闭写入(即没有执行document.close()
),document.write
写入的内容就会追加在已有内容的后面
document.writeln
方法与write
方法完全一致,除了会在输出内容的尾部添加换行符
document.querySelector
方法接受一个 CSS 选择器作为参数,返回匹配该选择器的元素节点。如果有多个节点满足匹配条件,则返回第一个匹配的节点。如果没有发现匹配的节点,则返回null
。
document.querySelectorAll
方法与querySelector
用法类似,区别是返回一个NodeList
对象,包含所有匹配给定选择器的节点.如果querySelectorAll
方法的参数是字符串*
,则会返回文档中的所有元素节点。另外,querySelectorAll
的返回结果不是动态集合,不会实时反映元素节点的变化。
document.getElementsByTagName()
方法搜索 HTML 标签名,返回符合条件的元素。它的返回值是一个类似数组对象(HTMLCollection
实例),可以实时反映 HTML 文档的变化。如果没有任何匹配的元素,就返回一个空集。
document.getElementsByClassName()
方法返回一个类似数组的对象(HTMLCollection
实例),包括了所有class
名字符合指定条件的元素,元素的变化实时反映在返回结果中
document.getElementsByName()
方法用于选择拥有name
属性的 HTML 元素(比如<form>
、<radio>
、<img>
、<frame>
、<embed>
和<object>
等),返回一个类似数组的的对象(NodeList
实例),因为name
属性相同的元素可能不止一个
document.getElementById()
方法返回匹配指定id
属性的元素节点。如果没有发现匹配的节点,则返回null
document.createElement
方法用来生成元素节点,并返回该节点
document.createTextNode
方法用来生成文本节点(Text
实例),并返回该节点。它的参数是文本节点的内容
document.createAttribute
方法生成一个新的属性节点(Attr
实例),并返回它
document.createComment
方法生成一个新的注释节点,并返回该节点
document.createDocumentFragment()
方法生成一个DocumentFragment实例
document.createEvent
方法生成一个事件对象(Event
实例),该对象可以被element.dispatchEvent
方法使用,触发指定事件
document.hasFocus
方法返回一个布尔值,表示当前文档之中是否有元素被激活或获得焦点
document.adoptNode
方法将某个节点及其子节点,从原来所在的文档或DocumentFragment
里面移除,归属当前document
对象,返回插入后的新节点。插入的节点对象的ownerDocument
属性,会变成当前的document
对象,而parentNode
属性是null
document.createNodeIterator
方法返回一个子节点遍历器
document.createTreeWalker
方法返回一个 DOM 的子树遍历器。它与document.createNodeIterator
方法基本是类似的,区别在于它返回的是TreeWalker
实例,后者返回的是NodeIterator
实例。另外,它的第一个节点不是根节点
document.queryCommandEnabled()
方法返回一个布尔值,表示当前是否可用document.execCommand()
的某个命令。比如,bold
(加粗)命令只有存在文本选中时才可用,如果没有选中文本,就不可用
实例属性
element.id
:属性直接返回指定元素的id属性
element.tagname
属性直接返回指定元素的大写标签名,与nodeName属性的值相等
element.dir
属性用于读写当前元素的文字方向,从左到右为ltr,从右到左为rtl
element.draggable
属性返回一个布尔值,表示当前元素是否可拖动
element.lang
属性返回当前元素的语言设置
element.hidden
属性返回一个布尔值,表示当前元素的hidden属性,用来控制当前的元素是否可见
element.className
属性用来读写当前元素节点的class属性
element.classList
属性返回一个类似数组的对象,当前节点的每一个class就算这个对象的一个成员
element.innerHTML
属性返回一个字符串,等同于该元素包含的所有html代码
element.outerHTML
属性返回一个字符串,表示当前元素节点的所有HTML代码,包括该元素自身和所有子元素
element.clientHeight
属性返回一个整数值,表示元素节点的css高度
element.clientwidth
属性返回元素节点的CSS宽度
element.clientLeft
属性等于元素节点左边框的宽度
element.clientTop
属性等于元素顶部边框的宽度
element.scrollHeight
属性返回一个整数值,表示当前元素的总高度,包括溢出容器不可见的高度、padding、伪元素的高度,不包括border、margin、以及水平滚动条的高度
element.scrollWidth
属性返回当前元素的总宽度
element.offsetHeight
属性返回一个整数,表示元素的CSS垂直高度,包括元素自身的高度、padding、border、以及水平滚动条的高度
element.offsetWidth
属性返回一个CSS水平宽度。
element.scrollLeft
属性表示当前元素的水平滚动条向右侧滚动的像素数量,如果没有滚动条值为0
element.scrollTop
属性表示当前元素的垂直滚动条向下滚动的像素数量,如果没有滚动条值为0
element.offsetLeft
属性表示当前元素左上角相对于element.offsetParent节点的水平位移
element.offsetTop
属性表示当前元素左上角相对于element.offsetParent节点的垂直位移
element.firstElementChild
属性返回当前元素的第一个元素子节点
element.lastElementChild
返回当前元素的最后一个元素子节点
element.offsetParent
属性返回最靠近当前元素的、并且CSS的position属性不等于static的上层元素
element.children
属性返回一个类似数组的对象,包含当前元素节点的所有子元素。如果当前元素没有子元素,则返回的对象包含零个成员
实例方法
element.querySelector()
方法接受CSS选择器作为参数,返回父元素的第一个匹配的子元素,如果没有找到匹配的子元素,返回null
element.querySelectorAll()
方法接受CSS选择器作为参数,返回一个NodeList实例,包含所有匹配的子元素
element.getElementsByClassName
方法返回一个HTMLCollection实例,成员是当前元素的子元素节点。与document.getElementByClassName类似,只是搜索范围不是整个文档是当前元素element
element.scrolltoView
方法滚动当前元素,进入浏览器的可见区域,类似于设置window.location.hash的效果
element.getBoundingClientRect()
方法返回一个对象,提供当前元素节点的大小、位置等信息,基本上是CSS盒状模型的所有信息
element.getClientRects()
方法返回一个类似数组的对象,里面是元素当前在页面上形成的矩形
element.insertAdjacentHTML()
方法用于将一个HTML字符串解析成DOM结构插入相对于当前节点的指定位置
element.remove()
方法用于将当前元素节点从它的父节点移除
element.focus()
方法用于将当前页面的焦点转移到指定元素上
element.blur()
方法用于将焦点从当前元素上移除
element.click()
方法用于在当前元素上模拟一次鼠标点击,相当于触发了click事件
document和element都是单个dom对象,可以使用htmlcollection和nodelist多节点对象
NodeList包含各种类型的节点,HTMLCollection只是HTML元素节点
NodeList实例很像数组,但是不是数组,不可以使用pop或者push等数组的方法,可以使用length属性和forEach方法,也可以使用for遍历
如果NodeList要使用数组的方法,可以将其转为真正的数组,使用array.slice.call(nodelist)进行转换
遍历时可以选择NodeList.keys、NodeList.values、NodeList.entries三个对象进行遍历。NodeList.keys返回键名的遍历器,NodeList.values返回键值的遍历器,NodeList.entries返回的遍历器同时包含键名和键值
for (var key of children.keys()) {
console.log(key)
}
for (var value of children.values()) {
console.log(value)
}
for (var entry of children.entries()) {
console.log(entry)
}
HTMLCollection是节点对象的集合,但只能包含元素节点(element),不能包含其他类型的节点。HTMLCollection与NodeList接口不同,HTMLCollection没有forEach方法,只能用for循环遍历
HTMLCollection.length属性:返回HTMLCollection实例包含的成员数量
HTMLCollection.Item()方法:接收一个整数值作为参数,返回该位置上的成员
HTMLCollection.namedItem()方法:通过id或者name属性返回对应的元素节点,如果没有对应的节点返回null
Text节点表示元素节点和属性节点的文本内容。如果一个节点只包含一段文本,那么它就有一个文本字节点,代表该节点的文本内容。
属性
text.data属性等同于NodeValue属性,用来设置或读取文本节点的内容
text.wholeText属性将当前文本节点和毗邻的文本节点作为一个整体返回
text.length属性返回当前文本节点的文本长度
方法
text.appendData方法用于在Text节点尾部追加字符串
text.deleteData方法用于删除Text节点内部的子字符串,第一个参数为子字符串开始的位置,第二个参数是子字符串长度
text.insertData方法用于在text节点插入字符串,第一个参数为插入位置,第二个参数是插入的子字符串
text.replaceData方法用于获取子字符串,第一个参数为子字符串在Text节点中的开始位置,第二个参数为子字符串长度
text.remove方法用于移除当前Text节点
text.splitText方法将Text节点一分为二,变成两个毗邻的text节点,它的参数是从0开始。如果位置不存在,将报错
DocumentFragment节点代表一个文档的片段,本身就是一个完整的DOM树形结构,它没有父节点,parentNode返回Null,但是可以插入任意数量的子节点,它不属于当前节点,所以操作DocumentFragment节点比直接操作DOM树快的多
DocumentFragment节点本身不能被插入当前文档,当他作为appendChild、insertBefore等方法的参数时,是它的所有子节点插入当前文档,而不是它自身。一旦DocumentFragment节点被插入,它自身就变成空节点,可以再次被使用。如果想要保留DocumentFragment节点的内容,可以使用cloneNode方法。
DocumentFragment节点不是单独的一种节点对象,它具有的属性和方法全部继承自Node节点和ParentNode接口。
storage 接口用于脚本在浏览器保存数据。两个对象部署了这个接口:window.sessionStorage
和window.localStorage
。
属性:
Storage.length
:返回保存的数据项个数。
方法:
Storage.setItem()
方法用于存入数据。它接受两个参数,第一个是键名,第二个是保存的数据。如果键名已经存在,该方法会更新已有的键值。
Storage.getItem()
方法用于读取数据。它只有一个参数,就是键名。如果键名不存在,该方法返回null
。
Storage.removeItem()
方法用于清除某个键名对应的键值。它接受键名作为参数,如果键名不存在,该方法不会做任何事情。
Storage.clear()
方法用于清除所有保存的数据。该方法的返回值是undefined
Storage.key()
接受一个整数作为参数(从零开始),返回该位置对应的键值
window.localStorage.setItem('baz', 'c');
window.sessionStorage.setItem('key', 'value');
window.localStorage.setItem('key', 'value');
window.sessionStorage.getItem('key')
window.localStorage.getItem('key')
sessionStorage.removeItem('key');
localStorage.removeItem('key');
window.sessionStorage.clear()
window.localStorage.clear()
window.sessionStorage.key(0)
//遍历所有键
for (var i = 0; i < window.localStorage.length; i++) {
console.log(localStorage.key(i));
}
window.addEventListener('storage', onStorageChange);
异步方法
JavaScript 语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方,就能使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当作参数传入其他函数,或者作为函数的结果返回。函数只是一个可以执行的值,此外并无特殊之处。
由于函数与其他数据类型地位平等,所以在 JavaScript 语言中又称函数为第一等公民。
return语句
函数体内的return语句表示返回,JavaScript引擎遇到return语句就会直接返回return语句后面那个表达式的值,后面即使还有语句也不会得到执行,也就是说,return语句所带的那个表达式就是函数的返回值。return语句不是必须的,如果没有的话该函数就不返回任何值,或者说返回undefined
通过return语句调用自己,就是递归,比如计算斐波那契数列
function fib(num) {
if(num == 0) return 0;
if(num == 1) return 1;
return fib(num-1) + fib(num-2)
}
属性
name属性
函数的name
属性返回函数的名字。如果变量的值是一个具名函数,那么name
属性返回function
关键字之后的那个函数名。如果是通过变量赋值定义的函数,那么name
属性返回变量名。
function f1() {}
f1.name // "f1"
var f2 = function () {};
f2.name // "f2"
var f3 = function myName() {};
f3.name // 'myName'
length属性
函数的length
属性返回函数预期传入的参数个数,即函数定义之中的参数个数
function f(a, b) {}
f.length // 2
toString方法
function f() {
a();
b();
c();
}
f.toString()
// function f() {
// a();
// b();
// c();
// }
arguements对象
由于 JavaScript 允许函数有不定数目的参数,所以需要一种机制,可以在函数体内部读取所有参数。这就是arguments
对象的由来。
arguments
对象包含了函数运行时的所有参数,arguments[0]
就是第一个参数,arguments[1]
就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。
arguments可以在函数内部进行修改,非严格模式下会和函数参数具有联动关系,即改变arguments会改变参数的值。严格模式下,arguments
对象与函数参数不具有联动关系。也就是说,修改arguments
对象不会影响到实际的函数参数
var f = function(a, b) {
arguments[0] = 3;
arguments[1] = 2;
return a + b;
}
f(1, 1) // 5
var f = function(a, b) {
'use strict'; // 开启严格模式
arguments[0] = 3;
arguments[1] = 2;
return a + b;
}
f(1, 1) // 2
通过arguments
对象的length
属性,可以判断函数调用时到底带几个参数
虽然arguments
很像数组,但它是一个对象。数组专有的方法(比如slice
和forEach
),不能在arguments
对象上直接使用。
如果要让arguments
对象使用数组方法,真正的解决方法是将arguments
转为真正的数组。下面是两种常用的转换方法:slice
方法和逐一填入新数组。
JavaScript 引擎的工作方式是,先解析代码,获取所有被声明的变量,然后再一行一行地运行。这造成的结果,就是所有的变量的声明语句,都会被提升到代码的头部,这就叫做变量提升(hoisting)。
console.log(a) //undefined
var a='我是谁'
console.log(a) //'我是谁'
例:变量提升常常会导致的错误,在for循环中添加异步事件时会导致var直接变成最后一个,因为for循环是同步循环,先完成循环再执行异步任务,使用let变量即使是异步事件由于局部作用域不会发生这种事
for (var i=0;i<5;i++) {
console.log(i);
}
// 输出:0,1,2,3,4
for (var i=0;i<5;i++) {
setTimeout(()=>{
console.log(i);
},0)
}
//当事件是异步时输出都是4,set
//使用立即执行函数或者let变量可以输出0,1,2,3,4
for (let i=0;i<5;i++) {
setTimeout(()=>{
console.log(i);
},0)
}
for (var i=0;i<5;i++) {
(function(i){
setTimeout(()=>{
console.log(i);
},0)
})(i)
}
变量提升的其他例子
var foo = 3;
// 预编译之后
function hoistVariable() {
var foo;
foo = foo || 5;
console.log(foo); // 5
}
hoistVariable();
JavaScript 引擎将函数名视同变量名,所以采用function
命令声明函数时,整个函数会像变量声明一样,被提升到代码头部。所以,下面的代码不会报错。
表面上,上面代码好像在声明之前就调用了函数f
。但是实际上,由于“变量提升”,函数f
被提升到了代码头部,也就是在调用之前已经声明了。但是,如果采用赋值语句定义函数,JavaScript 就会报错。
函数提升只针对具名函数,而对于赋值的匿名函数,并不会存在函数提升。
function hoistFunction() {
foo(); // output: I am hoisted
function foo() {
console.log('I am hoisted');
}
}
hoistFunction();
// 预编译之后
function hoistFunction() {
function foo() {
console.log('I am hoisted');
}
foo(); // output: I am hoisted
}
hoistFunction();
函数提升优先级高于变量提升,且不会被同名变量声明覆盖,但是会被变量赋值后覆盖。而且存在同名函数与同名变量时,优先执行函数。
函数声明被提升时,声明和赋值两个步骤都会被提升,而普通变量却只能提升声明步骤,而不能提升赋值步骤。变量被提升过后,先对提升上来的所有对象统一执行一遍声明步骤,然后再对变量执行一次赋值步骤。而执行赋值步骤时,会优先执行函数变量的赋值步骤,再执行普通变量的赋值步骤。
console.log(a); //f a() 此时a是函数
console.log(a()); //1
var a=1;
function a(){
console.log(1);
}
console.log(a); //1
a=3
console.log(a()) //a not a function,被覆盖
变量提升和函数提升的原因:
函数提升是为了解决函数相互递归调用的目的
也就是说,变量提升是人为实现的问题,而函数提升在当初设计时是有目的的。
其他:
ES6中的class声明也存在提升,不过它和let、const一样,被约束和限制了,其规定,如果再声明位置之前引用,则是不合法的,会抛出一个异常。
所以,无论是早期的代码,还是ES6中的代码,我们都需要遵循一点,先声明,后使用。
JS执行环境在JS机制内部就是用一个对象来表示的
,称作执行环境对象
,简称环境对象
。执行环境分为全局执行环境
和局部执行环境
两种,每个执行环境都有一个属于自己的环境对象。在web浏览器中,全局环境对象为window对象
作用域
作用域是变量或者函数可以被访问的代码范围,或者说是变量和函数所起作用的范围。
作用域分为全局作用域
、局部作用域
两种。
在页面中的脚本开始执行时,就会产生一个“全局作用域”。它是最外围(范围最大,或者说层级最高)的一个作用域。全局作用域的变量、函数 可以在代码的任何地方访问到。
当一个函数被创建的时候,会创建一个“局部作用域”。局部作用域中的函数、变量只能在某些局部代码中可以访问到。
作用域链
当前作⽤域没有定义的变量,就是⾃由变量 。为了得到⾃由变量,js程序内部将向⽗级作⽤域寻找。如果上一级父级作用域也没有,就一层一层向上找,直到找到全局作⽤域还是没找到,就宣布放弃。这种⼀层⼀ 层的关系,就是 作⽤域链 。
闭包就是有权访问另一个函数作用域中的变量的函数。
由于函数作用域的影响,正常情况下,函数外部无法读取函数内部声明的变量,只有函数内部可以读取全局变量和父作用域变量。
如果出于种种原因,需要得到函数内的局部变量。正常情况下,这是办不到的,只有通过变通方法才能实现。
在函数内部定义子函数,将子函数作为返回值,就可以在外部读取函数内部的变量,作为返回值的子函数称为闭包
function f1() {
var n = 999;
function f2() {
console.log(n);
}
return f2;
}
var result = f1();
result(); // 999
闭包的最大用处有两个,一个是可以读取外层函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。原因是闭包(上例的inc
)用到了外层变量(start
),导致外层函数(createIncrementor
)不能从内存释放。只要闭包没有被垃圾回收机制清除,外层函数提供的运行环境也不会被清除,它的内部变量就始终保存着当前值,供闭包读取。
此外,闭包的另一个用处,是封装对象的私有属性和私有方法。
function Person(name) {
var _age;
function setAge(n) {
_age = n;
}
function getAge() {
return _age;
}
return {
name: name,
getAge: getAge,
setAge: setAge
};
}
var p1 = Person('张三');
p1.setAge(25);
p1.getAge() // 25
闭包的使用场景
需要值长期保存又需要隐藏的场景
闭包的问题
一般情况下,一个函数执行完内部的代码,函数调用时所创建的执行环境、环境对象(包括变量对象、[[scope]]等)都会被销毁,它们的生命周期就只有函数调用到函数执行结束这一段时间。
闭包形成后,会在函数执行完仍将他的变量对象保存在内存中,当引用时间过长或者引用对象很多的时候,会占用大量内存,严重影响性能。
闭包的清除
将闭包的值手动置空即可。
eval命令
eval
命令接受一个字符串作为参数,并将这个字符串当作语句执行。
立即执行函数就是声明一个匿名函数,并且马上调用这个匿名函数
//函数最后的括号是调用的意思
(function() {alert('匿名函数')})()
立即执行函数的作用只有一个:创建独立的作用域
在这个作用域里面的变量,外面访问不到,即避免变量污染
var liList = ul.getElementsByTagName('li')
for(var i=0;i<6;i++){
liList[i].onclick = function(){
alert(i) //输出都是6,i贯穿整个作用域,而不是给每个li一个i
}
}
//使用立即执行函数创建独立作用域
var liList = ul.getElementsByTagName('li')
for(var i=0;i<6;i++){
!function(ii){
liList[ii].onclick = function(){
alert(ii) //输出都是6,i贯穿整个作用域,而不是给每个li一个i
}
}
}
调用
function foo() {
foo() // 是否存在堆栈溢出错误?
}
foo()
function foo() {
setTimeout(foo, 0); // 是否存在堆栈溢出错误?
};
function foo() {
return Promise.resolve().then(foo);
};
第一种调用方式,自己调用自己,堆栈会溢出
第二场调用方式,堆栈不会溢出。JavaScript并发模型基于“事件循环”。浏览器的主要组件包括调用堆栈,事件循环,任务队列和Web API。 像setTimeout
,setInterval
和Promise
这样的全局函数不是JavaScript的一部分,而是 Web API 的一部分
JS调用栈是后进先出(LIFO)的。引擎每次从堆栈中取出一个函数,然后从上到下依次运行代码。每当它遇到一些异步代码,如setTimeout
,它就把它交给Web API
(箭头1)。因此,每当事件被触发时,callback
都会被发送到任务队列(箭头2)
事件循环(Event loop)\不断地监视任务队列(Task Queue),并按它们排队的顺序一次处理一个回调。每当*调用堆栈(call stack)**为空时,\*Event loop**获取回调并将其放入**堆栈(stack )(箭头3)中进行处理。请记住,如果调用堆栈不是空的,则事件循环不会将任何回调推入堆栈**
因此第二种调用方式执行过程:
1.调用 foo()
会将foo
函数放入调用堆栈(call stack)。
2.在处理内部代码时,JS引擎遇到setTimeout
。
3.然后将foo
回调函数传递给WebAPIs(箭头1)并从函数返回,调用堆栈再次为空
4.计时器被设置为0,因此foo
将被发送到任务队列(箭头2)。
5.由于调用堆栈是空的,事件循环将选择foo
回调并将其推入调用堆栈进行处理。
6.进程再次重复,堆栈不会溢出。
第三种调用方式,渲染进程会被堵塞,无法响应UI
大多数时候,开发人员假设在事件循环\图中只有一个任务队列。但事实并非如此,我们可以有多个任务队列。由浏览器选择其中的一个队列并在该队列中**处理回调**
在底层来看,JavaScript中有宏任务和微任务。setTimeout
回调是宏任务,而Promise
回调是微任务。
主要的区别在于他们的执行方式。宏任务在单个循环周期中一次一个地推入堆栈,但是微任务队列总是在执行后返回到事件循环之前清空。因此,如果你以处理条目的速度向这个队列添加条目,那么你就永远在处理微任务。只有当微任务队列为空时,事件循环才会重新渲染页面
每次调用'foo
'都会继续在微任务队列上添加另一个'foo
'回调,因此事件循环无法继续处理其他事件(滚动,单击等),直到该队列完全清空为止。 因此,它会阻止渲染。
无法只省略该参数,而不省略它后面的参数,除非显式输入undefined
。如果传入undefined
,将触发该参数等于默认值,null
则没有这个效果。
function f(x, y = 5, z) {
return [x, y, z];
}
f() // [undefined, 5, undefined]
f(1) // [1, 5, undefined]
f(1, ,2) // 报错
f(1, undefined, 2) // [1, 5, 2]
数组的Map方法函数的占位符为下滑线
Array.from({ length: 31 }).map((_, i) => i + 1),
早期的 JavaScript 语言有很多设计不合理的地方,但是为了兼容以前的代码,又不能改变老的语法,只能不断添加新的语法,引导程序员使用新语法。
严格模式是从 ES5 进入标准的,主要目的有以下几个。
严格模式可以用于整个脚本,也可以只用于单个函数。use strict
放在脚本文件的第一行,整个脚本都将以严格模式运行。如果这行语句不在第一行就无效,整个脚本会以正常模式运行。use strict
放在函数体的第一行,则整个函数以严格模式运行。
进入严格模式的标志,是一行字符串use strict
。老版本的引擎会把它当作一行普通字符串,加以忽略。新版本的引擎就会进入严格模式。
严格模式不允许的语法:
严格模式下,设置字符串的length
属性,会报错。长度只可读,不可写;
严格模式下,对一个只有取值器(getter)、没有存值器(setter)的属性赋值,会报错。
严格模式下,对禁止扩展的对象添加新属性,会报错。
正常模式下,函数内部的this
可能会指向全局对象,严格模式禁止这种用法,避免无意间创造全局变量。
函数内部不得使用fn.caller
、fn.arguments
,否则会报错。这意味着不能在函数内部得到调用栈了。
严格模式下无法删除变量,如果使用delete
命令删除一个变量,会报错。只有对象的属性,且属性的描述对象的configurable
属性设置为true
,才能被delete
命令删除。
this
指向属性或方法“当前”所在的对象。
this
的动态切换,固然为 JavaScript 创造了巨大的灵活性,但也使得编程变得困难和模糊。有时,需要把this
固定下来,避免出现意想不到的情况。JavaScript 提供了call
、apply
、bind
这三个方法,来切换/固定this
的指向。
call
方法的参数是一个对象。如果参数为空、null
和undefined
,则默认传入全局对象。
var n = 123;
var obj = { n: 456 };
function a() {
console.log(this.n);
}
a.call() // 123
a.call(null) // 123
a.call(undefined) // 123
a.call(window) // 123
a.call(obj) // 456
apply
方法的作用与call
方法类似,也是改变this
指向,然后再调用该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数
bind()
方法用于将函数体内的this
绑定到某个对象,然后返回一个新函数。
var counter = {
count: 0,
inc: function () {
this.count++;
}
};
var func = counter.inc.bind(counter);
func();
counter.count // 1
JavaScript 语言之所以有 this 的设计,跟内存里面的数据结构有关系。
将一个对象赋值给变量obj
。JavaScript 引擎会先在内存里面,生成一个对象{ foo: 5 }
,然后把这个对象的内存地址赋值给变量obj
。也就是说,变量obj
是一个地址(reference)。后面如果要读取obj.foo
,引擎先从obj
拿到内存地址,然后再从该地址读出原始的对象,返回它的foo
属性。
原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。
这样的结构是很清晰的,问题在于属性的值可能是一个函数。
引擎会将函数单独保存在内存中,然后再将函数的地址赋值给foo
属性的value
属性。
由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行。JavaScript 允许在函数体内部,引用当前环境的其他变量。
现在问题就来了,由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this
就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。
new绑定: new方式是优先级最高的一种调用方式,只要是使用new方式来调用一个构造函数,this一定会指向new调用函数新创建的对象
显示绑定:显示绑定指的是通过call()和apply()方法,强制指定某些对象对函数进行调用,this则强制指向调用函数的对象
隐式绑定:隐式绑定是指通过为对象添加属性,该属性的值即为要调用的函数,进而使用该对象调用函数
默认绑定:默认绑定是指当上面这三条绑定规则都不符合时,默认绑定会把this指向全局对象window
function thisTo(){
console.log(this.a);
}
var a=2; //a是全局对象的一个同名属性
thisTo(); //2
全局环境
全局环境使用this
,它指的就是顶层对象window
。
this === window // true
function f() {
console.log(this === window);
}
f() // true
构造函数
// 定义了一个构造函数Obj。由于this指向实例对象,所以在构造函数内部定义this.p,就相当于定义实例对象有一个p属性。
var Obj = function (p) {
this.p = p;
};
var o = new Obj('Hello World!');
o.p // "Hello World!"
对象的方法
如果对象的方法里面包含this
,this
的指向就是方法运行时所在的对象。该方法赋值给另一个对象,就会改变this
的指向。
var obj ={
foo: function () {
console.log(this);
}
};
obj.foo() // obj
// 情况一
(obj.foo = obj.foo)() // window
// 情况二
(false || obj.foo)() // window
// 情况三
(1, obj.foo)() // window
引用赋值丢失:
当进行隐式绑定时,如果进行一次引用赋值或者传参操作,会造成this的丢失,使this绑定到全局对象中去。
function thisTo(){
console.log(this.a);
}
var data={
a:2,
foo:thisTo //通过属性引用this所在函数
};
var a=3;//全局属性
var newData = data.foo; //这里进行了一次引用赋值
newData(); // 3,因为newData实际上引用的是foo函数本身,这就相当于:var newData = thisTo;data对象只是一个中间桥梁,data.foo只起到传递函数的作用,所以newData跟data对象没有任何关系。而newData本身又不带a属性,最后a只能指向window。
回调函数传参丢失
传参丢失,就是在将包含this的函数作为参数在函数中传递时,this指向改变。setTimeout函数的本来写法应该是setTimeout(function(){......},100);100ms后执行的函数都在“......”中,可以将要执行函数定义成var fun = function(){......},即:setTimeout(fun,100),100ms后就有:fun();所以此时此刻是data.foo作为一个参数,是这样的:setTimeout(thisTo,100);100ms过后执行thisTo()
function thisTo(){
console.log(this.a);
}
var data={
a:2,
foo:thisTo //通过属性引用this所在函数
};
var a=3;//全局属性
setTimeout(data.foo,100);// 3
数组的foreach和map方法
foreach
方法的回调函数中的this
,其实是指向window
对象,因此取不到o.v
的值。原因跟上一段的多层this
是一样的,就是内层的this
不指向外部,而指向顶层对象。
解决this丢失的问题:
1.使用bind 方法返回一个新对象。它会把参数设置为this的上下文并调用原始函数。
2.间接引用:间接引用是指一个定义对象的方法引用另一个对象存在的方法,这种情况下会使得this指向window:
3.使用箭头函数:箭头函数在this这块是一个特殊的改进,箭头函数使用了词法作用域取代了传统的this机制,所以箭头函数无法使用上面所说的这些this优先级的原则,注意的是在箭头函数中,是根据外层父亲作用域来决定this的指向问题。
call方法第一个参数是this指向,第二个参数可以传入参数列表,call方法临时改变一次this指向,并立即执行
Apply方法可以传入参数数组,使用apply方法改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次。
bind方法和apply方法类似,第一个参数是this指向,第二个参数可以传入参数列表,但是bind改变this指向后不会立即执行,而是返回一个永久改变this指向的函数