Skip to content

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)等。

js
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的作用是从构造函数返回新对象。

js
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的情况。

js
Function.prototype.myBind = function(thisValue){
  thisValue._func = this
  return function(){
    return thisValue._func()
  }
}

这样我们就已经实现了bind最基本的功能——绑定this,但我们向thisValue添加了属性。其实我们只用保证结果正确,因此可以删除添加的属性,用临时变量替代。

js
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对象中找到,我们先看一下它的结构:

js
function printArguments(){return arguments}
printArguments(1,2,3)//[Arguments] { '0': 1, '1': 2, '2': 3 }

Arguments是一个特殊的对象,它不是数组,但具有数组的许多特征,这里把它当作数组。

先看function.call(thisArg,arg1,arg2,...),我们需要把后面的参数传入函数。

js
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

js
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作为例子。

js
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的模式。

js
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作为例子。

js
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接口。

js
let myIterable = {}
myIterable[Symbol.iterator] = function* (){
  yield 1;
  yield 2;
  yield 3;
}
[...myIterable]//[ 1, 2, 3 ]

借用数组的iterator:

js
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管理、与别人共享代码。

yarnyarn install

针对已有package.json或yarn.lock的项目,将所需依赖全部安装。

修改镜像源

yarn config set registry https://registry.npm.taobao.org/(全局)

yarn [operation] --reigistry https://registry.npm.taobao.org/(本次操作)

txt
//.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。

js
//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属性。

闭包

javascript
function outer() {
        let a = 1
        let inner = () => { a++; console.log(a) }
        return inner
      }

闭包利用了函数的执行环境,每次返回的inner都有不同的执行环境,意味着不同的inner分别拥有自己的a值。

PS:上次面头条,竟然没有写出来emm千万不要像我一样。

constructor 构造函数

原型链&继承

Promise

函数生成器

async...await

并发模型与事件循环

JavaScript的并发模型基于事件循环。

先同步,后异步。先执行微任务,后执行宏任务。

Stack, heap, queue

Stack 栈

这里的栈指函数调用形成的执行栈。函数具有参数和局部变量,如果函数A调用了函数B,并且执行函数A,那么函数A会被先压入栈,调用B时,函数B被压入栈(位于A之上),到函数B返回,其被弹出。

函数被压入栈的实际过程是压入调用帧。

Heap 堆

非结构化的存储区域,其中存储对象。

Queue 队列

JavaScript维护一个待处理的消息队列,而每一个消息与处理它的函数关联。在事件循环中的某个环节,JavaScript按顺序处理Queue的消息。

每当调用处理消息的函数,其形成的调用帧被压入栈。该函数可能会调用其他函数,因此只有当执行栈为空,JavaScript才能继续处理下一个消息。最终,消息队列为空。

事件循环

js
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一样需要被加入队列执行的异步代码,而微任务一定位于宏任务之前。

先祭上这段常见代码:

js
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之前被处理。

宏任务和微任务都存在于事件循环,但微任务尽管添加时间可能比宏任务晚,仍然要在下一个宏任务执行前执行。事件循环处理消息相当于有两个步骤,第一步检查当前是否有微任务(微任务虽然也是异步代码,但可以看作不在消息队列中,因为它会“插队”),如果有先完成,第二步执行宏任务并在队列中寻找下一个消息。

如果在宏任务执行过程中添加微任务,那么它会在下一个宏任务执行前执行。

js
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}

箭头函数的特性:

  1. 自动绑定初始化环境的this
  2. 没有arguments对象
  3. 不可以作为constructor,亦无prototype
  4. 不可作为generator函数

垃圾回收

每隔一段时间,系统或虚拟机搜索未被引用的变量,并释放其申请的空间。

装饰器

TypeScript

TS话题比较大,这里只做简单介绍。

参考

https://www.cnblogs.com/sunshq/p/7922182.html

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop

https://juejin.im/post/5b73d7a6518825610072b42b

用心去做高质量的内容网站,欢迎 star ⭐ 让更多人发现