你不知道的JS
2023-05-10 10:16:16 0 举报
js作用域与闭包及this和对象原型的学习记录
作者其他创作
大纲/内容
作用域和闭包
编译原理
分词/词法分析
字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元。例如:var a = 2;。被分为var、a、=、2、;。
解析/语法分析
将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树成为抽象语法树(Abstract Syntax Tree,AST)
代码生成
将AST转换为可执行的代码过程称为代码生成。
作用域是什么?
定义
根据名称查找标识符(变量或函数)的一套规则
特点
JS运行时产生,包含编译阶段(词法分析、语法分析和执行阶段的代码生成)和执行阶段
判断一个变量是否能被代码访问
JS代码编译的过程中作用域参与
执行的时作用域也会参与
作用域查找
在JS代码运行时,声明、访问、修改变量的一个集合;访问变量使用RHS查询;修改变量使用LHS查询;
RHS查询失败,抛出ReferenceError;严格模式下LHS查询失败也会抛出ReferenceError,
非严格模式下,LHS查询失败,隐式创建全局变量。TypeError表示作用域查找成功了,但是对查到的结果操作不正确。
非严格模式下,LHS查询失败,隐式创建全局变量。TypeError表示作用域查找成功了,但是对查到的结果操作不正确。
出现作用域嵌套,查询时会优先查询当前函数或者块级作用域,找不到时,会向上级作用域查找,
直到找到该变量停止或者抵达最上层作用域(全局作用域)停止
直到找到该变量停止或者抵达最上层作用域(全局作用域)停止
作用域嵌套
一个块或者函数内部包含另一个块或函数时,就发生了作用域嵌套
词法作用域(静态作用域)
词法阶段
词法阶段:大多数标准编译语言,在编译的第一个阶段叫作词法化(单词化)
定义在词法阶段的作用域。在写代码时将变量和块作用域写在哪里决定的,因此词法分析器处理代码时会保持作用域不变
遮蔽效应
遮蔽效应,作用域在查找时找到第一个匹配项便会自动停止;因此多级嵌套作用域中同名变量,内部变量会遮蔽外部变量。
注意:全局变量会自动成为window对象的属性,因此被遮蔽的全局变量可通过window.属性名获取,中间层级被遮蔽的变量将无法获取
注意:全局变量会自动成为window对象的属性,因此被遮蔽的全局变量可通过window.属性名获取,中间层级被遮蔽的变量将无法获取
欺骗词法
欺骗词法,正常情况下,词法作用域在写代码期间函数声明的位置来定义;但在JS中eavl和with可以欺骗词法作用域。
注意:欺骗词法作用域会导致性能下降
注意:欺骗词法作用域会导致性能下降
eval原理:接受一个字符串作为参数,并将里边的代码视为一开始就写在这个位置的代码,执行的过程中动态插入一段代码,没有经过编译阶段的词法分析;引擎在执行eval之后的代码时,并不知道这段代码是动态插入的。严格模式下,eval有自己的作用域,无法修改eval所处的词法作用域。不推荐使用。
with:提供一个快捷访问一个对向的所有属性,但同时它会在所处的词法作用域中创建一个全新的完全隔离的词法作用域,词法作用域就是这个对象;但是这个块内部正常的var却不会被限制在快内部,例如,这个对象中如果没有a属性,但是在with引用对象时如果创建了一个a,此时a就会被泄露到with本身所处的词法作用域中。注意:严格模式下with完全失效,不推荐使用。
总结
总结:js引擎在编译阶段,会做大量优化工作,在词法分析阶段,能够预先确定变量和函数的定义位置,在查找时能快速定位着找到变量或函数。
但使用了eval和with,引擎就会默认之前所做的位置优化都是无效的,会导致代码执行变慢,因此,不要轻易使用欺骗词法作用域的机制。
但使用了eval和with,引擎就会默认之前所做的位置优化都是无效的,会导致代码执行变慢,因此,不要轻易使用欺骗词法作用域的机制。
函数作用域
定义
属于这个函数内部的全部标识符(变量和函数)只能在这个函数内部或者嵌套在它内部的作用域中使用
思考:传统认知,声明一个函数,往里边写代码,反过来考虑,可以有一些启示?
既然函数能够创建一个封闭的作用域,那么可以在考虑将已经写好的任意个代码片段,用函数包起来,实现隐藏的效果。
考虑用函数将代码块隐藏,但是常规的已经function关键字开始声明的具名函数必然会将函数名字暴露出来,可能污染了目前这个作用域。
思考:有没有一种方式,既可以隐藏代码,又不用产生新的变量污染呢?
匿名函数表达式:(function(){}),没有名称标识符。
缺点:1、调试时,在栈追踪时,不会显示有意义的名字,调试很困难
2、自己调用自己时,比较困难,比如递归中。废弃的aruments.callee
3、代码的可读性和可理解性比较差。
2、自己调用自己时,比较困难,比如递归中。废弃的aruments.callee
3、代码的可读性和可理解性比较差。
函数类型
函数声明和函数表达式:凡是声明中function关键字的位置在开头时,就是函数声明,否则就是一个函数表达式。
立即执行函数
立即执行函数表达式:(function(){})(),具名立即函数表达式,(function IIFE(){})()
设计原则
软件设计中,有个最小暴露原则,应该最小限度的暴露必要的内容,而将其他内容隐藏起来。
规避冲突
规避冲突:在一个大的作用域下,可以避免同名标识符之间的冲突,导致代码意外覆盖。
块作用域
包含在一对闭合的花括号中的作用域
with、tyr/catch中的catch分句会创建一个块作用域。
let 关键字会将变量绑定在任意作用域中,通常事{。。。。}内部,let会为声明的变量隐士的劫持所在的块级作用域,而且声明也不会被提升。
使用let时,建议可以显示的为变量声明作用域,依据最小暴露原则来决定如何定义块作用域的范围。
使用let时,建议可以显示的为变量声明作用域,依据最小暴露原则来决定如何定义块作用域的范围。
let循环,let不仅将变量绑定到for循环的块中,并且,他将变量重新绑定到了循环的每次迭代中。
const,可以创建块作用域,但其值是固定的,后边修改值的操作都会引起错误。
提升
无论声明在作用域的声明位置,都会被移动到各自作用域的最顶端,这个过程就是提升。
总结:所有的声明提升,函数优先提升。根据编译阶段理解,重复声明,只会创建一次
闭包
JS中闭包无处不在,你需要能够识别,并拥抱它
闭包是基于词法作用域书写代码时所产生的自然结果
定义:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
函数在定义时的词法作用域以外被调用。闭包使得函数可以继续访问定义时的词法作用域。此时这个 函数仍然持有对该作用域的引用,而这个引用就叫作闭包。
只要使用了回调函数,实际上就是在使用闭包。
模块
模块模式必要条件
必须有外部的分闭函数,该函数至少被调用一次(每次调用都会创建一个新的模块实例)
封闭函数必须至少返回一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态
现在的模块机制
未来的模块机制
this和对象原型
关于this
误解
指向自身:误认为函数的this,指向函数的自身。
它的作用域:this指向函数的作用域。
this到底是什么?
this是运行时绑定的,并不是编写代码时绑定,它的上下文取决于函数调用时各种条件。与函数声明的位置无关,只取决于函数调用方式。
执行上下文
当函数被调用的时,会创建一个活动记录,这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息,这个记录成为执行上下文。而this就是这个记录的一个属性。
this全面解析
调用位置
定义:函数在代码中被调用的位置。
调用栈
可以把函数的调用链理解为调用栈
绑定规则
默认绑定
函数是直接使用不带任何修饰的函数引用进行调用,就会引用默认绑定规则。this将默认绑定到全局对象上。
隐式绑定
调用位置是否有上下文对象,必须在对象内部包含一个指向函数引用的属性,并通过这个属性间接引用函数,从而把this隐式绑定到这个对象上。
在对象属性引用链中只有紧挨着函数引用的那一层在调用位置起作用。
隐式丢失
在传递的过程中,通常会丢失this,因为传递时,通常值传递了函数的引用,在调用时可能不会带着上下文修饰,因此会应用默认绑定规则。
显示绑定
直接指定this的绑定对象,这种称为显式绑定。
call/apply
思考:显示绑定是否能够解决绑定丢失的问题?
硬绑定
创建一个包裹函数,负责接收和返回值
另一种使用方法是创建一个可以重复使用的辅助函数
ES5提供的bind方法
API 调用的上下文
在许多第三方库或JS和宿主环境中提供了需要内置函数,提供了可选参数,可用来指定this的绑定对象。
new绑定
构造函数
传统的面向类的语言中,构造函数是类中的一些特殊方法,使用new初始化类时会调用类中的构造函数。
JS中是不存在所谓的构造函数,并不是属于某个类,也不会实例化一个类,只是被new操作符调用的普通函数。称为函数的构造调用。
使用new操作符
1、创建或者构造一个全新的对象;
2、这个新对象会被执行[[Prototype]]连接;
3、这个对象会绑定到函数调用的this;
4、如果这个函数没有返回其他对象,那么new 表达式中的函数调用会自动返回这个新对象
2、这个新对象会被执行[[Prototype]]连接;
3、这个对象会绑定到函数调用的this;
4、如果这个函数没有返回其他对象,那么new 表达式中的函数调用会自动返回这个新对象
优先级
new绑定>显示绑定>隐式绑定>默认绑定
1、函数是否在new中调用?如果是 this绑定的是一个新创建的对象;
2、函数是否通过call、apply或者硬绑定调用?如果是 this绑定的是一个指定的对象;
3、函数是否在某个上下文对象中调用?如果是 this绑定的是哪个上下文对象;
4、如果都不是,使用默认绑定,如果在严格模式下,绑定到undefined,否则绑定到全局对象。
2、函数是否通过call、apply或者硬绑定调用?如果是 this绑定的是一个指定的对象;
3、函数是否在某个上下文对象中调用?如果是 this绑定的是哪个上下文对象;
4、如果都不是,使用默认绑定,如果在严格模式下,绑定到undefined,否则绑定到全局对象。
绑定例外
将null、undefined作为this的绑定对象传入时,值在调用时默认忽略,直接应用默认规则。
函数柯里化
使用bind(),进行柯里化。
更安全的this
在需要忽略this绑定的时候,可以传入一个DMZ对象(一个空的非委托对象)var % = Object.create(null);
间接引用
可能会创建一个函数的间接引用,在这种情况下调用函数时,就会应用默认绑定规则。
最容易在赋值时发生
function foo(){
console.log(this.a)
}
var a = 2;
var o = {
a:2,
bar:foo
}
var p ={a:4}
var b = (p.bar = o.bar);
b();
console.log(this.a)
}
var a = 2;
var o = {
a:2,
bar:foo
}
var p ={a:4}
var b = (p.bar = o.bar);
b();
软绑定:封装指定的函数,首先检查this的指向,如果是undefined或者全局对象时,就将传入的对象绑定到this,否则不会修改this。
this的词法
ES6提供了箭头函数,使用胖箭头的操作符 =>定义函数,箭头函数不适用以上四种规则,而是根据外层作用域来决定this。
对象
语法
两种定义方式:字面量声明式和构造形式
类型
typeof null = 'object',实际上null就是基本类型。
原理:不同的对象在底层都表示为二进制,在js中二进制前三位都为0的话会被判定为object类型,null的二进制表示全是0,自然前三位也是0,所以执行typeof 时会返回'object'。
内置对象
js中有一些对象的子类型,通常被称为内置对象。
String、Number、Boolean、Object、Function、Array、Date、RegExp、Error
内容
存储
在js引擎中对象的属性值,一般并不会存在对象容器内部,存储在对象容器内部的只是这些属性名称,它们就像指针一样(技术角度来说就是引用),指向这些值真正的存储位置
.a中 . 操作符常被称为属性访问;["a"]中 [] 操作符常被称为键访问。
区别:. 操作符要求属性名满足标识符的命名规范,而 [""] 语法可以接受任意utf8/unicode字符串作为属性名。
可计算属性名
ES6增加了可计算属性名,可以在文字形式中使用 [] 包裹一个表达式作为属性名。var obj = { ["a"+"b"]:"hellow" }
属性与方法
js中函数永远不会属于一个对象,即使在对象的文字形式声明一个函数表达式,这个函数也不会属于这个对象;它们只是对于相同函数对象的多个引用。(存储可以解释这一点)
数组
复制对象
浅拷贝
深拷贝
属性描述符
0 条评论
下一页