js红宝书

第十章 函数

10.1 箭头函数

没有参数和多个参数需要括号,如果不使用大括号,那么箭头后面只有一行代码。这行代码可以是一个赋值操作或表达式。省略大括号会隐式返回这行代码,这行代码不能用return。

10.2 函数名

所有函数对象都会暴露一个只读的name属性,其中包含函数的信息。多数情况下保存的是一个函数标识符(字符串化的变量名)。

匿名函数也会显示成空字符串。如果是使用Function构造函数创建的,标识为”anonymous”。如果是获取,设置函数或使用了bind()实例化,那么标识符前面会加一个前缀。

10.3 理解参数

在使用function关键字定义函数时,可以在函数内部访问arguments这个类数组对象,获得传进来的每个参数值(可以不写形参(命名参数),而通过arguments[0]等来访问传进来的参数,所以开发者想传多少参数就传多少参数)。

修改arguments对象的值会自动同步到命名参数,但他们还不是访问同一个内存地址,在严格模式下重写arguments对象会导致语法错误。修改命名参数不会影响arguments的值,它始终以传入的参数为准。

虽然箭头函数没有arguments对象,但可以在包装函数中(function函数包裹箭头函数)把它提供给箭头函数。

10.4没有重载

因为ECMAScript函数没有函数签名,不存在验证命名参数的机制,自然就没有重载,定义两个同名函数,后定义的会覆盖先定义的。

10.5默认参数值

ECMAScript6支持显式定义默认值,不需要写三元运算符判断,传入了就用传入的,没传入就用默认参数。

arguments对象不会反映参数的默认值,只反映传给函数的参数。

参数初始化顺序遵循“暂时性死区”,即前面定义的参数不能引用后定义的参数。

10.6参数扩展与收集

扩展操作符可以用于既可以用于函数调用时传参也可以用于定义函数参数。在普通函数和箭头函数中,可以用于命名参数,同时也可以使用默认参数(比如…[1,2,3]传给(a,b,c=1))。

可以使用扩展操作符把独立参数组合成一个数组,类似arguments对象的构造机制,只不过收集参数的结果会得到一个Array实例,可以用数组中的方法。funcrtion(…values){values.reduce()}

10.7函数声明和函数表达式

js在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数的定义,这叫函数声明提升。而函数表达式必须执行到那一行才在执行上下文中生成函数定义。

10.8函数作为值

函数在ECMAScript中就是变量,所以函数可以用在任何可以使用变量的地方。

函数接受两个参数,第一个参数是一个函数,第二个参数是传给这个函数的值。

也可以在函数中返回一个函数,比如使用sort函数是想传入一个函数根据不同属性来排序,外层函数是传入属性值,内层函数返回的判断大小的函数)。

10.9函数内部

  1. arguments

    arguments.callee指向arguments对象所在函数的指针,可以用于递归函数的函数逻辑与函数名解耦。

    在你递归调用时函数使用这个,可以确保无论通过什么变量调用这个函数都不会出问题,防止后面这个函数赋给不同名的变量,这个变量执行调用而找不到函数中的使用原函数名的函数。

  2. this

    在标准函数中,this引用的是把函数当成方法调用的上下文对象。

    在箭头函数中this引用的是

  3. caller

    引用调用当前函数的函数,在全局作用域中调用的则为null

  4. new.target

    ECMAScript 中的函数始终可以作为构造函数实例化一个新对象,也可以作为普通函数被调用。
    ECMAScript 6新增了检测函数是否使用new关键字调用的new.target属性,如果函数是正常调用的,则new.target 的值是undefined;如果是使用new关键字调用的,则new.target将引用被调用的构造函数。

10.10函数属性和方法

属性

  1. length

    保存函数定义的命名参数的个数

  2. prototype

    保存引用类型所有实例方法的地方

方法

  1. apply()

  2. call()

    上面两种方法都是控制调用该方法的函数的函数体内this值的能力

    直接传arguments对象或者一个数组,用apply(),否则用call()

  3. bind()

    该方法会创建一个新的函数实例,其this值会被绑定到传给bind的对象

10.11函数表达式

函数表达式和函数声明的区别主要是函数声明提升,不要在语句(如条件判断语句)中使用函数声明,不能正确返回声明,可在语句外声明一个变量,在语句内给变量赋值匿名函数(即函数表达式)

10.13尾调用优化

尾调用优化条件

递归尾调用优化

10.14闭包

闭包是指引用了另一个函数作用域中变量的函数,通常是在函数嵌套中实现的

函数执行时,每个执行上下文中都有一个包含其中变量的对象,全局上下文中的叫变量对象,函数局部上下文中的叫活动对象。

在调用函数时,创建执行上下文,然后创建作用域链(包含指针的列表),闭包的作用域链有3个指针:全局变量对象,外部函数(形成闭包的那个外部函数)的活动对象,(闭包)内部函数的活动对象。

副作用:外部函数在它执行完毕之后执行上下文的作用域链会被销毁,但是它的活动对象仍然保留在内存中,因为内部函数的作用域中仍然有对它的引用,直到内部函数被销毁才会被销毁。

this对象

函数在被调用时会自动创建两个特殊变量:this和arguments。内部函数永远无法直接访问外部函数的这两个变量,所有如果要使用this.外部函数属性,可能并不能访问到,而是直接返回的window.属性名,解决方法:将this保存到可以访问的另一个变量中

内存泄漏

内存泄漏常常与闭包紧紧联系在一起,很容易让人误以为闭包就会导致内存泄漏。其实闭包只是让内存常驻,而滥用闭包才会导致内存泄漏。
内存泄漏,从广义上说就是,内存在使用完毕之后,对于不再要的内存没有及时释放或者无法释放。不再需要的内存使用完毕之后肯定需要释放掉,否则这个块内存就浪费掉了,相当于内存泄漏了。但是在实际中,往往不会通过判断该内存或变量是否不再需要使用来判断。因为内存测试工具很难判断该内存是否不再需要。所以我们通常会重复多次执行某段逻辑链路,然后每隔一段时间进行一次内存dump,然后判断内存是否存在不断增长的趋势,如果存在,则可用怀疑存在内存泄漏的可能。

第十一章

js是单线程 优势:实现简单 缺点:语句block,会引发后续的阻塞
异步编程:异步函数不阻塞后续任务的执行

​ JavaScript中的执行是基于一个称为”调用栈”(call stack)的概念的。调用栈是一个数据结构,用于存储函数调用的信息。当一个函数被调用时,其信息(如函数的参数、局部变量等)被推送到栈的顶部,形成一个新的栈帧。当函数执行完成时,对应的栈帧被弹出,控制权返回到调用该函数的地方。如果是console.log()这样的语句压入栈中,它会立即执行立即弹出。

​ 在同一时间,只有一个栈帧处于活动状态。当发生异步操作(例如定时器、事件监听器等)时,这些操作被放入任务队列,等待执行。一旦调用栈为空,事件循环将从队列中取出任务,并将其推送到调用栈,开始执行。

异步函数压入栈中立马弹出(不会阻塞),把异步的回调传给任务队列

1
2
3
4
5
6
7
//面试题
function xiao(){
console.log("xiao")
}
setTimeout(xiao,100);//正常延迟
setTimeout(xiao(),100);//setTimeout失效,没有延迟,而是立刻执行xiao。浏览器会说你的回调函数undefined,即setTimeout(undefined,100)
setTimeout('xiao()',100)//传入字符串,当传入字符串时,字符串会被解释为 JavaScript 代码并执行。立即输出

经典异步编程方式

后面的函数依赖前面的异步函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
foo(callback){
setTimeout(()=>{
callback()
},8000)
}
foo(run())//run函数依赖foo执行完成之后才自己执行。实现了run在异步函数setTimeout等待完成之后再执行
//缺点:多重依赖不利于维护,各模块高度耦合


//解决:经典异步编程方式
//1.事件监听-点对点通信
//事件监听是以方法为中心,利用方法的挂载和监听实现点对点通信
foo.on('finished',run())
foo(){
setTimeout(()=>{
foo.trigger('finished')//foo触发finiehed就触发run,foo和run通过finished方法来建立连接
},800)
}
//2.自定义事件
//声明事件
let event=new customEvent('myTestEvent',{
'detail':{

},
'bubbles':true,
'cancelable':false
})
//监听事件
document.addEventListener('myTestEvent',e=>{
console.log(e)
})
//触发事件
document.dispatchEvent(event)

//3.发布、订阅
//以信号为中心,每当异步事件完成,就发出相应的信号。所有订阅该信号的方法,都会收到通知并触发相应的处理

//4.promise方式
foo.then(run())
function foo(){
return new Promise(resolve=>{
setTimeout(function(){
resovle()
},500)
})
}


现代化异步编程

增加了两种异步编程方式:

1.async&await无损的切换同步和异步代码(加上或不加await即可),对上面的promise方式做的改良

//async函数返回一个promise,返回promise函数做await

2.步进 iterator&generator

第十四章 DOM

Array.prototype.slice 是一个数组的原型方法,用于创建一个数组的浅拷贝。然而,NodeList 对象并不是一个真正的数组,它是类数组对象。类数组对象通常具有数值索引和 length 属性,但它们不具备数组对象上的方法,如 slice

通过 Array.prototype.slice.call 的方式,我们实际上在借用 Array 对象上的 slice 方法,并将 NodeList 对象作为 this 上下文。这样,slice 方法就会以 NodeList 对象为基础,返回一个包含相同元素的新数组。

这种方法被称为借用或强制。通过 call 方法,我们可以在一个对象上调用属于另一个对象的方法,并将其作为当前对象的方法来执行。这是 JavaScript 中一种常见的技巧,可以在不修改原始对象的情况下使用其他对象的方法。

eval() 是 JavaScript 中的一个全局函数,它用于执行一段字符串中的 JavaScript 代码。具体来说,eval() 接收一个字符串参数,将其解析为 JavaScript 代码并执行。这可以用于动态地生成和执行代码。

然而,使用 eval() 是需要谨慎的,因为它有一些潜在的安全和性能问题:

  1. 安全性问题: eval() 可以执行任何合法的 JavaScript 代码,包括对全局对象的修改。如果 eval() 执行的代码包含来自不可信源的内容,可能会引入安全漏洞,例如代码注入攻击。
  2. 性能问题: 使用 eval() 会影响性能,因为它在运行时动态解析和执行代码。现代 JavaScript 引擎通常会优化静态代码,但对于动态生成的代码,优化的效果会受到限制。
  3. 作用域问题: eval() 执行的代码可以访问调用它的函数的作用域,这可能导致意外的变量覆盖和作用域问题。

使用场景:

  1. 监测DOM变化: 当需要监测某个 DOM 元素及其子元素的变化时,MutationObserver 是一个强大的工具。这包括元素的添加、删除、属性变化等。
  2. 实时UI更新: 当需要在用户界面中实时反映 DOM 的变化时,例如,通过插入或删除元素来更新 UI。
  3. 表单验证: 可以使用 MutationObserver 监测表单元素的变化,以便在用户填写表单时执行实时验证或其他操作。
  4. 单页应用(SPA): 在单页应用程序中,页面的内容可能会在不重新加载整个页面的情况下发生变化。MutationObserver 可以用于监测内容变化并作出相应的处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 创建一个 MutationObserver 实例,并指定回调函数
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
console.log(mutation.type); // mutation 的类型:attributes、childList、characterData
console.log(mutation.target); // 发生变化的节点
});
});

// 配置观察的目标节点和观察的类型
var config = { attributes: true, childList: true, subtree: true };

// 传入目标节点和配置进行观察
observer.observe(document.body, config);

// 随后,当 body 发生变化时,回调函数将被触发