JavaScript进阶
数字
- Number(n: number)与new Number(n: number)分别创建了number与object类型变量(通常没有必要使用Number类型实例)
- toString(radix: number)接受2、8、10、16(2到36),用于转换进制
- toFixed(n: number)可以定义小数点后位数,用于转换格式
- toPrecision()自动应用toFixed()或toExponential()(固定小数位数或科学计数法的系数)
函数
- 必需参数使用参数列表,可选参数用对象封装
- arguments.callee指向函数本身,可以避免递归调用与函数名耦合(当然其他语言中不存在这问题)
- [function].caller指向调用函数的函数
- this指向调用函数的对象
- 函数一定包括length(命名参数个数)与prototype属性
- 函数也支持toString方法,返回值是代码字符串(这种行为没有严格规定,不同环境可能不同)
- 用func.apply(this,[array])将接受多个参数的函数转换为接受数组
数组
- 通过设置length,可以动态增长/缩减数组,超过length的项==undefined
- 访问array[n],如果length<n,数组长度自动扩展至n
this
this指向调用当前函数的对象,如果没有对象调用,那么this==window。在箭头函数中,this指向与环境中相同。bind、call、reply方法都可以修改this。
绑定this
绑定this的4种方法,按优先级从低到高排序分别是默认绑定、隐式绑定、显式绑定、new绑定。其中apply()与call()属于显式绑定。
默认绑定
全局环境中、函数独立调用时(即使被嵌套、包括立即执行函数),this指向window。有时候被嵌套的函数想获得上层函数的this,可以使用var that = this
语句传递this值。
隐式绑定/方法调用
this的值是调用该函数的对象。如a.b()中b内部的this指向a。只有直接调用obj.func()
时才会传递this,否则this仍指向window。函数虽然可以属于一个对象,但函数不会与对象绑定this。
这些不能传递this的场景包括赋值、传参、内置函数(如setTimeout)、间接引用(对象的属性赋值时的立即执行函数,如(p.foo = o.foo)()
,this为window)等。
var val='window'
var func = function(){
console.log(this.val)
}
var obj={val:'obj',func:func};
func()//window
obj.func()//obj
//赋值会丢失this
var func1 = obj.func
func1()//window
//作为参数传递也会丢失this
var func2 = function(func){
func()
}
func2(obj.func)//window
显示绑定
即使用call、apply、bind方法绑定this到对象上。JS还新增了许多内置函数提供thisValue选项,如数组的迭代方法map、forEach、filter、some、every。
new绑定
new的作用是从构造函数返回新对象。
function constructorA(){
this.a=0
}
let obj = new constructorA()
obj.a//0
需要注意如果constructor没有返回值,那么构造的对象就是返回值。而其中的this永远指向obj,即新生成的对象,即使let newObj = new obj.constructorA()
,this也会指向newObj,而不是obj。
this与函数
this的四种绑定方法其实也对应函数的四种调用方式,包括函数调用(func)、方法调用(obj.func)、间接调用(call、apply)、构造函数调用(new)。
实现bind
Function.prototype.bind(thisArg[,arg1[,arg2[,...]]])
bind提供了两个功能,一个是传递this,另一个是传递参数(之后传参数会位于这些参数后面),它返回绑定后的函数。
首先,我们要传递this,需要通过方法调用来完成,bind的this是原函数,我们要怎么返回绑定好的函数呢?🤔
可以想到,我们先把函数绑定到那个环境上去,但这就是bind的功能啊。实际上,我们应该返回一个全新函数,而这个新函数的执行结果与原函数+thisArg一致。
那么先不考虑传递参数,只考虑this的情况。
Function.prototype.myBind = function(thisValue){
thisValue._func = this
return function(){
return thisValue._func()
}
}
这样我们就已经实现了bind最基本的功能——绑定this,但我们向thisValue添加了属性。其实我们只用保证结果正确,因此可以删除添加的属性,用临时变量替代。
Function.prototype.myBind = function(thisValue){
let func = this
let getResult = (func,thisValue)=>{
thisValue._func = func
let result = thisValue._func()
delete thisValue._func
return result
}
return function(){
return getResult(func,thisValue)
}
}
细心的朋友不难看出,getResult和call、apply有着相似的功能,即返回绑定某个this值后的执行结果。也就是说,bind其实是基于call和apply实现的。但为了方便理解整个概念,我先介绍了bind。
call和apply
明白了基本原理之后,我们来研究如何传入参数。首先,我们应该知道所有参数都可以在aruguments对象中找到,我们先看一下它的结构:
function printArguments(){return arguments}
printArguments(1,2,3)//[Arguments] { '0': 1, '1': 2, '2': 3 }
Arguments是一个特殊的对象,它不是数组,但具有数组的许多特征,这里把它当作数组。
先看function.call(thisArg,arg1,arg2,...)
,我们需要把后面的参数传入函数。
Function.prototype.myCall = function(){
let [thisValue,...parameters] = arguments
thisValue._func = this
let result = thisValue._func(...parameters)
delete thisValue._func
return result
}
再看看function.apply(thisArg,[argsArray])
,我们转换一下参数形式就行。需要注意我们的argsArray不能为空,如果为空,parameters为undefined,再使用扩展运算符就会报错is not iterable
。
Function.prototype.myApply = function(){
let [thisValue,parameters] = arguments
thisValue._func = this
let result = thisValue._func(...parameters)
delete thisValue._func
return result
}
可以完善的一些地方
如果没有传this值,我们可以让默认值为window,如果没有传argsArray,让默认值为[],这里拿apply作为例子。
Function.prototype.myApply = function(){
let [thisValue,parameters] = arguments
if(!thisValue)thisValue = window
if(!parameters)parameters = []
thisValue._func = this
let result = thisValue._func(...parameters)
delete thisValue._func
return result
}
我们的bind也可以改造成使用apply的模式。
Function.prototype.myBind = function(){
let func = this
let [thisValue,...parameters] = arguments
return function(){
return func.myApply(thisValue,parameters)
}
}
现在还可以继续考虑两个问题:如果不能使用…运算符怎么办?如果绑定的对象已经有了_func属性或者不能设置属性怎么办?
先看如何替代…运算符。如果不能使用…运算符,我们就不能方便地向函数传递任意多个参数,可以使用eval()解析生成的字符串,也可以用new Function([arg1[,arg2[,...argN]],]functionBody)
结合读取arguments数组读取任意多个参数,也是通过解析生成的字符串。
使用不重复的属性。ES6提供了symbol()用于标志唯一的事物,它可以以作为对象的属性,我们仍以apply作为例子。
Function.prototype.myApply = function(){
let [thisValue,parameters] = arguments
if(!thisValue)thisValue = window
if(!parameters)parameters = []
let _func = Symbol()
thisValue[_func] = this
let result = thisValue[_func](...parameters)
delete thisValue[_func]
return result
}
此外还可以用Math.random()生成一个不太可能重复的属性名。
作用域 Scope
作用域即变量和函数的可访问范围和生命周期。
变量作用域
可以分为全局作用域、函数内作用域(包括嵌套函数)、块级作用域。局部变量可以在局部覆盖同名全局变量。
作用域链 Scope Chain
从外层到函数乃至嵌套函数内,多个执行环境形成了一个程序执行栈,同时也形成了一个指向多个环境的链表,即作用域链。作用域链包括VO与所有上级的作用域。
作用域链决定了函数能访问变量的范围,组织了访问变量的顺序,在解析标识符时一级一级地按顺序查找函数和变量。需要注意的是,JS根据函数定义的位置查找,而非执行的位置。
执行环境/执行上下文 Execution Context
执行环境定义了变量与函数能访问的数据,包括全局、函数内、eval、块级作用域等。
执行环境包括至少3个重要的属性:作用域链、变量对象和this(按创建顺序排序)。这里关注前两个。
变量对象 Variable Object
每个执行环境都有与之关联的变量对象,它包括了环境中所有的变量和函数。全剧环境下的变量对象称为VO。
所有的变量和函数即函数声明、变量声明、函数形参。不包括匿名函数。
活动对象 Activation Object
当环境是函数时,活动对象就是变量对象。活动对象至少包含arguments对象(即函数参数)。有很多资料提到函数中不能直接访问VO,而以AO替代,不过反正这两个都是不能用JS代码打印出来的。
Arguments对象至少包括callee(对当前函数的引用)、length(真正传递参数的个数)、properties-indexs(函数的参数值),注意Arguments并不包含this。
with语句
with (expression) statement
with语句算是语法糖,statement中的语句可以使用expression中的任意属性而不必加上前缀。你也可以自己实现一个函数,接受一个对象,将其方法的this绑定到对象上,然后定义若干个和对象属性相同的变量,从而用method()访问obj.method()。不过必须要用到eval函数,因为js不能直接动态命名变量。
with存在性能开销,大型项目不建议使用。
iterable接口 ES6
对象的[Symbol.iterator]
属性指向其默认的遍历器,其不仅提供了统一的接口,还能按规定的顺序排列。在ES6中,Array、String、Set和Map、arguments等类数组对象都具有iterable接口。而解构赋值、扩展运算符、yield和任何与接受数组的场合都调用了Iterator接口。
let myIterable = {}
myIterable[Symbol.iterator] = function* (){
yield 1;
yield 2;
yield 3;
}
[...myIterable]//[ 1, 2, 3 ]
借用数组的iterator:
let fakeArray={0:'apple',1:'peach',2:'pancake',length:3,
[Symbol.iterator]:Array.prototype[Symbol.iterator]}
for(let it of fakeArray){console.log(it)}//apple peach pancake
待填坑 async写法、next()写法
模块管理
NPM
Node Package Manager。默认安装到项目目录下,-g安装到全局,-save在package.json写入dependencies字段,-save-dev相应写入devDependencies字段。
YARNRecommend
NPM与YARN使用几乎无差异,但YARN是NPM的改进版,主要支持了并行安装、离线模式(从缓存安装)、统一版本。因此不建议再使用NPM。
yarn add <package>
添加包,yarn global add <package>
添加全局包,yarn add <package> --dev
添加dev依赖。yarn添加的依赖会默认保存到package.json里。
初始化
yarn init
init会引导你创建package.json文件。没有init也可以安装依赖,但package.json可以只存储包名和版本,方便git管理、与别人共享代码。
yarn
或yarn install
针对已有package.json或yarn.lock的项目,将所需依赖全部安装。
修改镜像源
yarn config set registry https://registry.npm.taobao.org/
(全局)
yarn [operation] --reigistry https://registry.npm.taobao.org/
(本次操作)
//.npmrc 项目内
registry=https://registry.npm.taobao.org
管理依赖包
首先,我们用<package>
、<package>@<version>
和<package>@<tag>
描述一个依赖包。
yarn upgrade <package>
(升级)
yarn remove <package>
(移除)
import与require
import与require都提供引入一个模块的功能,但require是AMD规范下的引入,在运行时调用,而import是ES6规定的引入,编译时调用(因此实际上最早执行,)。require对应exports,import对应export。
//CommonJS/AMD
const app = require("app")
module.exports = app
exports.app = app
//ES6规范,解构要求标识符对应(但可以用as重命名),引入export default这种的可以自定义变量名,一个文件可以同时export default和export xxx
import app from 'app'// export default xxx
import {login,logout} from 'app'//export const xxx or export function xxx or export {login,logout,...} or export * from 'xxx'
import * from 'xxx'
import {login as logIn} from 'app'
import app ,{login} from 'app'
import * as app from 'app'
原型 Prototype ✏️
JavaScript中的继承。原型法设计的思想是从类A扩展出类B,B以A为原型,具有A的属性与方法。JavaScript中每一个对象都有prototype属性。
闭包
function outer() {
let a = 1
let inner = () => { a++; console.log(a) }
return inner
}
闭包利用了函数的执行环境,每次返回的inner都有不同的执行环境,意味着不同的inner分别拥有自己的a值。
PS:上次面头条,竟然没有写出来emm千万不要像我一样。
constructor 构造函数
原型链&继承
Promise
函数生成器
async...await
并发模型与事件循环
JavaScript的并发模型基于事件循环。
先同步,后异步。先执行微任务,后执行宏任务。
Stack 栈
这里的栈指函数调用形成的执行栈。函数具有参数和局部变量,如果函数A调用了函数B,并且执行函数A,那么函数A会被先压入栈,调用B时,函数B被压入栈(位于A之上),到函数B返回,其被弹出。
函数被压入栈的实际过程是压入调用帧。
Heap 堆
非结构化的存储区域,其中存储对象。
Queue 队列
JavaScript维护一个待处理的消息队列,而每一个消息与处理它的函数关联。在事件循环中的某个环节,JavaScript按顺序处理Queue的消息。
每当调用处理消息的函数,其形成的调用帧被压入栈。该函数可能会调用其他函数,因此只有当执行栈为空,JavaScript才能继续处理下一个消息。最终,消息队列为空。
事件循环
while (queue.waitForMessage()){
queue.processNextMessage();
}
瞧,这就是事件循环,因为它是一个处理消息的循环。其中waitForMessage是同步的,如果没有消息,它就会等。
不打断地执行
如果你理解了队列的执行方式,那么你会明白这种处理方式意味着函数执行决不会被抢占。(相对于C/C++多线程,你不得不考虑函数被中断的情况)这为编程和分析带来了便利,但代价是消息处理函数可能会长时间阻塞其他事件,如用户的点击、滑动,在这种情况下,浏览器会提示无响应,用户可以选择等待或结束进程。
不阻塞
MDN声称JavaScript“永不阻塞”,这当然是不对的,例如alert()
与同步XHR的场景,但如此声称有它的理由。JavaScript中I/O通常采用事件回调的形式完成,这意味着I/O不会影响其余代码执行。
添加消息
事件需要绑定监听器以被监听,否则事件将丢失。例如用户点击按钮并被监听到时,消息队列就多了一个消息。
setTimeout(handler, timeOut)允许向队列添加消息,并且设置最小触发延时。延时可能大于设定的时间,因为预定的时间内JavaScript可能正在处理其他消息(即使延时设置为0也一样,并且H5标准规定最小间隔为4ms)。一个简单的例子是,先设定一个定时执行的函数,再令JavaScript进入无限循环,无论何时被设定的函数都不会执行。
同步代码
JavaScript的同步执行代码可以理解成第一条消息的处理函数,在它执行完前,不会有其他消息被处理。
Runtime间通信
JavaScript虽然是单线程,但跨域iframe和web worker都是独立的runtime。他们能且只能用postMessage()
发送消息,并监听message
事件。
宏任务与微任务
微任务和宏任务指的是setTimeout一样需要被加入队列执行的异步代码,而微任务一定位于宏任务之前。
先祭上这段常见代码:
setTimeout(_ => console.log(4))
new Promise(resolve => {
resolve()
console.log(1)
}).then(_ => {
console.log(3)
})
console.log(2)
Promise是同步代码,Promise.then才是异步代码,所以1,2的顺序是毫无疑问的。3,4都是异步任务,为什么3在4前面呢?如果以事件队列理解,4应该在3前面,但由于3是微任务,4是宏任务,3应该在4之前被处理。
宏任务和微任务都存在于事件循环,但微任务尽管添加时间可能比宏任务晚,仍然要在下一个宏任务执行前执行。事件循环处理消息相当于有两个步骤,第一步检查当前是否有微任务(微任务虽然也是异步代码,但可以看作不在消息队列中,因为它会“插队”),如果有先完成,第二步执行宏任务并在队列中寻找下一个消息。
如果在宏任务执行过程中添加微任务,那么它会在下一个宏任务执行前执行。
setTimeout(_ => {
Promise.resolve().then(_ => { console.log("Micro") });
console.log("Macro")
})
//Macro
//Micro
let two = (date) => { while (Date.now() - date < 2000) { } }
let twoWithPromise = (date) => {
Promise.resolve().then(_ => console.log('Promise'));
while (Date.now() - date < 2000) { }
}
let fdd = () => {
let d = Date.now();
setTimeout(twoWithPromise, 0, d);
setTimeout(two, 0, d);
setTimeout(two, 0, d)
}
//2秒后输出Promise,说明twoWithPromise的确花了2s,之后Promise.then执行,再之后才是下一个setTimeout
我在掘金上看到有人说requestAnimationFrame()的触发要先于setTimeout(),他说这是因为修改DOM属性是同步操作,这显然是不对的,同步只是注册监听器。参考评论,理想情况下requestAnimationFrame对于60Hz的显示器来说每16.6ms执行一次,而setTimeout(handler,0)既可能是4ms执行一次,也可能由于页面重新渲染,最小间隔变为16ms。当屏幕刷新率变高,requestAnimationFrame将在setTimeout()之前。
宏任务与微任务表格
函数/过程 | 宏任务 | 微任务 |
---|---|---|
I/O | ✅ | |
setTimeout | ✅ | |
setInterval | ✅ | |
Promise.then/catch/finally | ✅ | |
setImmediate(NodeJS) | ✅ | |
requestAnimationFrame(Browser) | ✅ | |
process.nextTick(NodeJS) | ✅ | |
MutationObserver(Browser) | ✅ |
防抖&节流
AJAX&Fetch
箭头函数
(arguments)=>{statement}
箭头函数的特性:
- 自动绑定初始化环境的this
- 没有arguments对象
- 不可以作为constructor,亦无prototype
- 不可作为generator函数
垃圾回收
每隔一段时间,系统或虚拟机搜索未被引用的变量,并释放其申请的空间。
装饰器
TypeScript
TS话题比较大,这里只做简单介绍。
参考
https://www.cnblogs.com/sunshq/p/7922182.html
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop