《深入理解现代JavaScrpt》阅读笔记
块级作用域let和const
区别 | let | const | var |
---|---|---|---|
不能重复声明,会报SyntaxError错 | const 定义常量,值不能修改的变量叫做常量,一定要赋初始值,因为不能修改。 | 可以重复声明 | |
块级作用域 | 拥有 | 拥有 | 不拥有 |
会不会污染全局变量(挂载在window上) | 不会(不是全局变量的属性) | 不会 | 会(是全局变量的属性) |
作用域和作用域链
作用域:一个代码段所在的区域,是静态的,在编写代码时就确定了。
作用:变量绑定在这个作用域内有效,隔离变量,不同作用域下同名变量不会有冲突。
作用域分类
- 全局作用域
- 函数作用域:函数的作用域在声明的时候就已经决定了,与调用位置无关
- 块级作用域
作用域链:多个作用域嵌套,就近选择,先在自己作用域找,然后去就近的作用域找。
提升和暂时性死区
var
声明的变量会使声明被提升到顶部,let
和const
也存在变量提升,只是提升的方式不同。
var
变量提升:变量的声明提升到顶部,值为undefined
let
、const
变量提升: 变量声明提升到顶部,只不过将该变量标记为尚未初始化
//原代码
function fn(){
console.log(answer); //undefined
var answer=42;
}
//变量提升
function fn(){
var answer;//声明提前
console.log(answer);
answer=42;
}
let、const的暂时性死区
let
和 const
存在暂时性死区
,代码执行过程中的一段时间内,在此期间无法使用标识符,也不能引用外层作用域的变量。
let answer;
function fn(){
//如果此时没有将变量变量提升到这里,answer应该取外层answer的值
console.log(answer); //Uncaught ReferenceError: Cannot access 'answer' before initialization
let answer=42;
}
//理解暂时性死区是暂时的与时间相关
function temporalExample(){
const f = ()=>{
console.log(value)//这里不会报错
}
let value = 42;
f(); //调用时,value已经声明
}
块和函数一样存在暂时性死区
function temporalExample(){
let p = '外部声明';
if(){//代码块 if while for for-in for-of ....
console.log(p)//报错
let p = '内部声明' //块内的声明取得了p标识符的使用权
}
}
每次循环迭代都有自己的块级作用域,类似同一个函数的不同调用都有自己的局部变量一样
function loops(){
for(let counter = 1;counter<=3;++counter){//每次循环都会创建一个counter变量,每次迭代都有自己的counter
}
}
函数
逗号运算符
逗号运算符是计算其左操作数,丢弃结果。然后计算其右操作时,并将该值最为结果。
function returnSecond(a,b){
return a,b;
}
returnSecond(1,2) //返回2
使用场景:逗号左边可以包含具有副作用的表达式(进行其他操作,但不需要返回值)
//一般函数
handles = handlers.map(function(handler){
unregister(handler);//进行一些操作,不需要返回值
return register(handler.iterm) ;
})
//箭头函数
handles = handlers.map(handler=>{
unregister(handler);//进行一些操作,不需要返回值
return register(handler.iterm) ;
})
//箭头函数与逗号运算符结合
handles = handlers.map(handler=>(unregister(handler),register(handler.iterm)))
ES2017支持在形参和实参列表的末尾添加一个逗号
function example(a,b,){} //形参个数仍然是2
example(2,1,)
函数的属性
参数的个数:函数名.length
函数名.length
获取函数的形参个数,默认值不会增加函数的arity(形参的数量),还会阻止后续形参被计算在内。
function fn(a,b=32,c){}
console.log(fn.length)//1
函数的name属性
//函数声明
function foo(){}; console.log(foo.name)//"foo"
//函数表达式
const f = function bar(){}; console.log(f.name) //"bar"
const fn = ()=>{}; console.log(fn.name) "fn"
//例外,将函数赋给现有对象的属性时,没有name属性,因为这种特定用法暴露了过多信息
const obj ={}; obj.foo=()=>{}; console.log(obj.foo.name)//""
arguments和rest参数
arguments | rest |
---|---|
类数组 | 数组 |
箭头函数的一个局部变量 | 箭头函数和非箭头函数都可以使用 |
包含所有实参,还会有一些附加属性(如callee属性) | 只包含那些没有对应形参的实参 |
类数组格式与数组结构类似,拥有 length
属性,可以通过索引来访问或设置里面的元素,但是不能使用数组的方法。
类的新特性
class 学习笔记:https://blog.csdn.net/qq_41370833/article/details/135292235
class可以看作只是一个语法糖,只是让对象原型的写法更加清晰,更像面向对象编程的语言。class是构造函数的另外一种写法。
class类
声明按照let/const
声明的方式被提升,类的特点与let/const
相似。
语法特点
- 如果不显示提供构造函数,默认提供空构造函数。构造函数必须通过
new
和Reflect.construct
调用 - 原型方法不可枚举,且JS引擎不会给他添加
prototype属性
和关联属性
。
ES5中,所有函数都可能被用作构造函数,所以需要添加一个与对象关联的prototype属性。 - 类的所有方法都定义在类的prototype属性上面。
class Color{
//构造函数
constructor(r=0,g=0,b=0){//三个数据属性(r,g,b)
this.r = r;
this.g = g;
this.b = b;
}
//一个访问器属性(rgb)
get rgb(){}
set rgb(value){}
//原型方法
toString()
//静态方法:放在Color类上,不放在Color.prototype上
static fromCss(css){}
}
const c = new Color();
console.log(typeof c.toString.prototype);//"undefined"
可计算方法名[]
中括号属性访问器语法[]
:类似属性访问器的用法
使用场景:创建的方法是在运行时确定名称的,而不是在代码中给定的。
- 可放入任意表达式
- 表达式在类定义时计算
- 如果表达式的结果不是
Symbol
或字符串,将会被转化成一个字符串。对象的key只能是字符串或Symbol类型,Symbol类型可以防止字符串格式key相同的问题 - 结果被用作方法名
let name = "foo" + Math.floor(Math.random()*1000)
class SomeClass{
[name](){
console.log("1")
}
}
const s = new SomeClass()
s[name]()
访问类属性的两种方式
点属性访问器:object.property property会被解析成“property”,所以点运算符后面不能跟变量。点运算符后面的property为有效标识符时,点运算符才能正常运行。
方括号属性访问:object[‘property’] 方括号属性里的表达式会被自计算,所以里面可以使用变量
继承extends
原型链是隐式原型链,对象隐式原型的值=对应构造函数的显式原型的值
继承对于JavaScript来说就是子类继承父类拥有的方法、属性、静态方法等。
class Parent{
constructor(name){
this.name = name;
}
}
class Child extends Parent{
constructor(name, age){
super(name);
this.age = age;
}
}
- 子类继承父类的静态方法/属性:子类构造器(Child)的隐式原型
__proto__
指向父类构造函数(Parent) - 子类继承父类的方法:子类的显式原型对象(Child.prototype)的隐式原型
__proto__
指向父类的显式对象(Parent.prototype) - 子类构造器里调用父类构造器,继承父类的属性:子类构造函数Child继承父类构造函数Parent的里的属性,使用
super调
用的。
// 1.构造器原型链:继承静态方法
Child.__proto__ === Parent; // true
// 2.实例原型链:继承显示原型上的方法
Child.prototype.__proto__ === Parent.prototype; // true
两条隐式原型继承链
- Child -> Parent -> Function.prototype -> Object.prototype
- Child.prototype -> Parent.prototype -> Object.prototype
关键字super
关键字super的两种使用场景
- super():在子类构造器中,把super当作一个函数来调用,创建对象并让其执行超类的初始化流程
- super.property 和 super.method(): 通过
super
访问超类的原型属性和方法
HomeObject字段
定义在类里的方法使用[[HomeObject]]
字段说明该方法定义在什么对象上,但是赋值给属性的传统函数形式则没有该字段。
super方法的调用流程
1.在b.test内会运行super.test(),也就是说该super对应b.test的[[HomeObject]]的原型。
2.b.test的[[HomeObject]]是B.prototype
,B.prototype
的隐式原型是A.prototype
3.super指的是A.prototype
,所以在A.prototype
寻找test方法
class A {
test(){}
}
Class B extends A{
test(){ return "B" + super.test()}
}
const b = new B();
b.test();
new.target 函数的调用方式
new.target
可以知道函数的调用方式,适用场景:定义抽象类、最终类等
- 函数被直接调用,new.target的值为undefiend
- 当前函数是new操作符的直接目标,new.target指向的是当前函数
- 当前函数通过super调用的,new.target指向最终调用的子类
function test1(){
console.log(new.target);
}
test1();//undefined
class Base{
constructor(){
console.log(new.target.name);
}
}
new Base();//"Base"
class Sub extends Base{
constructor(){
super();
}
}
class Super extends Sub{
constructor(){
super();
}
}
new Super();//super
类声明与类表达式
- 类的声明的特点
- 将类的名称添加到当前作用域中
- 结尾的大括号不需要加分号
- 类似let和const的作用域提升规则
- 类的表达式的特点
- 不会将类的名称添加到当前作用域中,赋值的结果是一个函数(类的构造函数)
//类的声明
class Class1{}
//类的表达式
let Color = class{};
//类的匿名表达式
let C = class Color2{};
conselo.log(Color2); //Color2 is not defined
console.log(typeof C)//"function"
对象的新特性
可计算属性名
ES5中,如果想创建一个属性名为变量的对象,必须要创建这个对象,然后为该对象添加属性。
let age = "age";
let obj = {};
obj[age ] = 18
新增可计算属性名[表达式]
let age = "age";
let obj = {
[age]:18
};
获取和设置对象的隐式原型
Object.setPrototypeOf(对象a,对象b)
:设置对象a的__proto__指向对象b
Object.getPrototypeOf(对象)
:获取指定对象的__proto__指向的值,没有返回null
由于实例对象的隐式原型 = 构造函数的显式原型,这里也可以说获取到该对象构造函数的显式原型。
实例对象的隐式原型 = 构造函数的显式原型,所以一般说对象的原型指的都是对象的隐式原型。
浏览器环境中的__proto__
属性
__proto__
是非标准扩展,在非浏览器环境的JavaScript引擎中,该方法不存在,所以不要使用,可以选择Object.setPrototypeO
方法代替。
对象方法的简写语法以及类之外的super
const obj ={
name:"joe",
say(){
console.log(this.name);
}
};
obj.say()
通过function进行属性初始化 | 通过方法简写语法进行属性初始化 |
---|---|
可以用作构造函数 | 方法没有指向原型对象的prototype 属性,所以不能用作构造函数 |
没有指向定义对象(宿主对象)的链接 | [[HomeObject]] 建立从函数到其宿主对象的链接 |
定义在对象里的方法使用[[HomeObject]]
字段说明该方法定义在什么对象上,但是赋值给属性的传统函数形式则没有该字段。(同类方法一致)
建立对象方法到其宿主对象链接的目的是在方法中使用super
关键字
1.获取当前函数的[[HomeObject]]
值,比如obj
2.获取对象当前的隐式原型,比如obj.proto
3.查找并使用原型上的对应属性与方法
const obj={
toString(){
return super.toString().toUpperCase();
}
}
Object.setPrototypeOf(obj,{
toString(){
return "a"
}
})
console.log(obj.toString()); //A
Symbol
Symbol
是原始数据类型,用于创建一个独一无二的值
使用场景:对象的属性名/属性键类型:Symbol | string
使用:调用Symbol
方法创建一个全新且唯一的Symbol
值。调用symbol.description
可以查看Symbol
的描述信息,只是单纯的描述不包含其他信息,不同的Symbol
可以有相同的描述。
const mySymbol = Symbol("添加该Symbol的描述信息");
const obj = {
[mySymbol] :6
}
对象的新增方法
Object.assign(target, …sources) 复制多个对象
语法:Object.assign(target, ...sources)
说明
1.将一个或多个源对象复制到目标对象,返回修改后的目标对象。
2.如果目标对象中存在相同的key则后面的覆盖前面的。
3.只拷贝源对象可枚举的自有属性
4.该方法会跳过值为null
或undefined
的参数
4.如果赋值期间出错,例如如果属性不可写,则会抛出 TypeError;如果在抛出异常之前已经添加了一些属性,则这些属性会被保留,而 target 对象也会被修改。
5.浅拷贝,基本类型直接赋值,引用类型复制地址。
const obj1 = { a: 0, b: { c: 0 } };
const obj2 = Object.assign({}, obj1);
console.log(obj2); // { a: 0, b: { c: 0 } }
obj1.a = 1;
console.log(obj1); // { a: 1, b: { c: 0 } }
console.log(obj2); // { a: 0, b: { c: 0 } }
obj2.a = 2;
console.log(obj1); // { a: 1, b: { c: 0 } }
console.log(obj2); // { a: 2, b: { c: 0 } }
obj2.b.c = 3;
console.log(obj1); // { a: 1, b: { c: 3 } }
console.log(obj2); // { a: 2, b: { c: 3 } }
Object.is(比较对象1,比较对象2) 比较两个值是否为相同值
Object.is
的机制类似====
,感觉Object.is
更合理一点?
区别在以下两个地方
console.log(Object.is(+0,-0)); //false
console.log(+0===-0); //true
console.log(Object.is(NaN,NaN)); //true
NaN === NaN; //false
Object对象的遍历
方法 | 描述 | 遍历不可枚举属性 | 遍历继承属性 | 遍历Symbol属性 |
---|---|---|---|---|
Object.keys(obj) Object.values(obj) Object.entries(obj) | 返回给定对象的自身可枚举属性组成的数组 | × | × | × |
for-in | 遍历对象的属性,包括原型链上的可枚举属性 | × | √ | × |
Object.getOwnPropertyNames() | 返回对象的自身属性的属性名,包括不可枚举属性 | √ | × | × |
Object.getOwnPropertySymbols() | 只获取对象的Symbol类型的属性键 | × | × | √ |
Object.getOwnPropertyDescriptors() | 获取对象自身的全部属性 使用场景:将该方法返回的对象通过 Object.definePropertis() 传递给另一个对象 | √ | × | √ |
Reflect.ownKeys(obj) | 获取对象的属性键,包含不可迭代的key和symbol类型的key | √ | × | √ |
Object.keys() = for in + Object.hasOwnProperty()
Reflect.ownKeys() = Object.getOwnPropertyNames() + Object.getOwnPropertySymbols()
let data = { name: 'tim', age: 18, [Symbol()]: 123 };
console.log(Object.keys(data)); // [ 'name', 'age' ]
console.log(Object.values(data)); // [ 'tim', 18 ]
console.log(Object.entries(data)); // [ [ 'name', 'tim' ], [ 'age', 18 ] ]
对象遍历的次序规则
1.遍历所有数值键,按照数值升序排列。
2.遍历所有字符串键,按照加入时间升序排列。
3.遍历所有 Symbol
键,按照加入时间升序排列。
Reflect.ownKeys({ [Symbol()]:0, b: 0, 10: 0, 2: 0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]
ES2019新增Object.fromEntries()
:把键值对列表转换为一个对象
语法:Object.fromEntries(iterable);
参数:iterable
,类似Array
、 Map
或者其它实现了可迭代协议的可迭代对象。
说明:Object.fromEntries()
执行与 Object.entries
互逆的操作。
Map.prototype.entries返回的列表恰好是Object.fromEntries的参数
const obj = Object.fromEntries([
["a",1],
["b",2],
["C",3]
]);
console.log(obj); // {a:1,b:2,c:3}
Symbol.toPrimitive 强制类型转换制算法优先调用
[Symbol.toPrimitive](倾向转换的类型)
:将对象转换为原始值的内置方法,强制类型转换制算法优先调用对象该属性的方法
参数类型:number
数字类型,string
字符串类型,default
没有倾向
返回值:该方法必须返回一个原始值,否则会报错。返回值不一定要与暗示值匹配,暗示只是暗示。
为什么新增该方法?
在对象中将数据转换为原始类型的操作中,传统的方式带有一定的暗示作用。
toString()
:倾向与使用字符串的场景
valueOf()
:用于其他场景与数字或者没有倾向的场景 --无法区分倾向数字或没有倾向
有关于类型的隐式转换
使用加法运算符,意味没有倾向
使用减法运算符,倾向于数字类型
String(obj)等,倾向于字符串类型
const object1 = {
[Symbol.toPrimitive](hint) {
if (hint === 'number') {
return 42;
}
return null;
},
};
如果对象没有[Symbol.toPrimitive]属性
没有 @@toPrimitive
属性的对象通过以不同的顺序调用 valueOf()
和 toString()
方法将其转换为原始值,
[Symbol.toPrimitive](hint){
let methods = hint === "string" ? ["toString","valueOf"] : ["valueOf","toString"]
for (const methodName if nmethods){
const method = this[methodName ]
if(typeof method === "function"){
const result = method.call(this);
if(result === null || typeof result !=="object")
return result;
}
}
throw new TypeError();
}
将对象转换为原始值
无偏向:[@@toPrimitive]("default")
→ valueOf()
→ toString()
偏向转换为数字:[@@toPrimitive]("number")
→ valueOf()
→ toString()
偏向转换为字符串:[@@toPrimitive]("string")
→ toString()
→ valueOf()
属性顺序 - 不建议依赖对象属性顺序
属性范围:自身属性
属性顺序的应用范围:ES15-19
没有应用到for-in
、Object.keys
、Object.values
、Object.entries
,ES20
中for-in
已经增加属性顺序。
1.数字字符串类型的键,数字顺序排序
2.其他字符串类型的键,按照对象属性的创建顺序
3.Symbol类型,按照对象属性的创建顺序