前言
一共有三篇,这一篇总结复习一下基础知识。包括js中的作用域链和原型链、this的指向、对象的一些特性与方法。
内容来源于《你不知道的js》《阮一峰ES6入门》《JavaScript语言精粹》《JavaScript高级程序设计》《JavaScript设计模式》《JavaScript模式》《MDN web文档》
本博客没有什么有价值的知识,仅作总结梳理之用,初学者可以看看。
起步
编辑器:vscode
插件:Quokka
可以在语句后直接出结果不需要运行或者log。所以代码都用
return
来返回结果
对象
属性
虽然在代码中常用foo.a
来访问属性值,但是还有[]
这种方式,计算属性名或Symbol属性名都要用这种方式访问
let a = 'a'
let s1 = Symbol('c')
let myObj = {
[a+'b']: 'Hello',
[s1]: 'World'
}
myObj['ab'] //? Hello
myObj[s1] //? World
Reflect.ownKeys(myObj) //?['ab',Symbol(c)]
函数对象
JS中函数是对象的子类型,区别在于函数可以被调用。因为函数是对象,所以函数可以像其他任何值一样被使用,可以保存在对象、变量中,可以被当作参数传给其他函数。可以拥有自己的属性,并且属性也可以是函数,可以函数调用函数。
函数被创建时有几个自带的属性,值得注意的是prototype
属性每个函数都有这个属性即使没有将此函数进行过new构造调用。
function foo() {
return 2
}
Object.getOwnPropertyDescriptors(foo)
{ length:
{ value: 1,
writable: false,
enumerable: false,
configurable: true },
name:
{ value: 'foo',
writable: false,
enumerable: false,
configurable: true },
arguments:
{ value: null,
writable: false,
enumerable: false,
configurable: false },
caller:
{ value: null,
writable: false,
enumerable: false,
configurable: false },
prototype:
{ value: foo {},
writable: true,
enumerable: false,
configurable: false } }
方法与引用类型
当一个对象的属性是一个函数的时候,普遍称之为对象的方法。当调用这个函数时就是调用了这个对象的方法。实际来看,函数并不真的属于某个对象,只是对象有一个指向某个函数对象的指针。
同样的在对象的属性中,保存对象、数组等和函数一样都是保存的引用。
这就导致随便设一个变量取得指向此属性值的指针,它修改后的结果都会影响原对象。因为共用的一个值,当复制对象的时候如果简单的复制下属性,也会导致新对象与原对象共用一个值,两者互相影响。
let foo = {
a: [1,2,3]
}
let bar = foo.a
bar.push(4)
foo.a //? [1,2,3,4]
属性描述符
从上文获取函数属性时可以看出,属性有一些自己的性质。所有的属性都用 属性描述符(Property Descriptors)
来描述,譬如foo.a
,因为它仅持有一个数据值所以又可以称为“数据描述符”。下面的属性可以用Object.defineProperty
来进行设置
- 可写性(Writable)控制改变属性的能力
- 可配置性(Configurable)控制是否可以配置设置为false则是不可以的单向操作
- 可枚举型(Enumerable)控制着一个属性是否能在特定的对象属性枚举操作中出现
当设置为false即使可以访问也不会被枚举
Getter和Setter
在foo.a
属性访问的时候实际上是在对象上进行了[[Get]]
操作,如果没有找到对象属性就会沿着原型链向上寻找,什么都没找到会返回undefined
,同理也有个用来设置或创建属性的操作。JS中用来操作这两个行为的函数称之为取值函数(getter)和存值函数(setter)
当将一个属性定义为拥有getter或setter,那么它的定义就成为了“访问器描述符”(与“数据描述符”相对),
let foo = {
get a() {
return this.b
},
set a(arg) {
this.b = arg*2
}
}
foo.a = 2
foo.b //? 4
foo.a //? 4
枚举与迭代
- Object.keys() 返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性)
- for..in.. 循环遍历对象自身的和继承的可枚举属性(不含Symbol属性)
- Object.getOwnPropertyNames(obj) 返回一个数组,包含对象自身的所有属性(不含Symbol属性,但是包括不可枚举属性).
- Reflect.ownKeys(obj) 返回一个数组,包含对象自身的所有属性,不管属性名是Symbol或字符串,也不管是否可枚举.
一些方法
罗列一下后面要用到的
Object.create(proto, [propertiesObject]) 以proto为原型创建个新对象并加入到原型链中去
Object.getPrototypeOf() 返回指定对象的原型(内部_Prototype_属性的值)。
Object.getOwnPropertyDescriptors(obj) 用来获取一个对象的所有自身属性的描述符。
Object.hasOwnProperty() 方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性
Object.isPrototypeOf() 方法用于测试一个对象是否存在于另一个对象的原型链上。
Object.setPrototypeOf() 方法设置一个指定的对象的原型 ( 即内部_Prototype_属性)到另一个对象因为性能问题不要直接设置而是用create创建。
for...in 语句以任意顺序遍历一个对象的可枚举属性。对于每个不同的属性,语句都会被执行。
Object.entries()返回一个数组,其元素是与直接在object上找到的可枚举属性键值对相对应的数组。属性的顺序与通过手动循环对象的属性值所给出的顺序相同。
作用域与闭包
作用域
作用域控制着变量与参数的可见性和生命周期。
JS中的作用域可以理解成词法作用域 就是在词法分析时被定义的作用域。换句话说,也就是在写代码时,变量写在哪,作用域就由此决定。
function foo(a) {
let b = a * 2;
function bar(c) {
console.log( a, b, c );
}
bar(b * 3);
}
foo( 2 ); // 2 4 12
上述代码作用域有三层,最内层bar中只包含c但是却可以访问上层的b。依靠的是在JS存在的作用域链,当寻找标识符时,在作用域链的规则下,从里向外寻找,找到第一个匹配停止,所以导致了内层访问外层和内层对外层同名标识符造成遮蔽。
闭包
闭包:函数可以访问它被创建时所处的上下文环境,被称为闭包。
闭包:闭包是指有权访问另一个函数作用域中变量的函数。
闭包:函数依然有对创建时环境的引用,这个引用称之为闭包。
三种闭包的定义,让人分不清闭包是函数、引用还是行为?但是意思都差不多
let fn
function foo() {
let a = 2
function baz() {
console.log( a )
}
fn = baz
}
foo()
fn() //2
可以看到fn保留了baz创建时的作用域,假设分为三层,fn1、foo2、baz3。那过程就是在1层的fn通过在3层的baz访问了在2层的a。
this指向
this
的作用是传递对象引用,也就是改变函数运行的上下文(context)。
前面写在JS中的作用域是词法作用域,但是this却和动态作用域类似
let obj = {
id: "obj",
sayId() {
console.log(this.id)
}
}
let id = "global"
obj.sayId() //obj
===================
let obj = {
id: "obj",
sayId() {
console.log(id)
}
}
let id = "global"
obj.sayId() //global
当没用this时,就是普通的运行函数,即使函数是某个对象的方法,作用域也是在书写时就确认了的。
这个函数在全局作用域中运行,取得的值也是全局作用域的。
但如果用了this那就会取调用点的值。
丢失this
也就是说代码没能如自己写的那样this指向。为什么会出现下面的情况呢。因为在传递函数参数时,发生了隐含的引用赋值
let obj = {
id: "obj",
sayId() {
console.log(this.id)
}
}
let id = "global"
obj.sayId() // obj
setTimeout( obj.sayId, 100 ) //undefined
=============
let bar = obj.sayId
bar() //? undefined
传参时就发生了这样的引用函数没有被对象调用而是在全局作用域中运行。所以this丢失
五种绑定
- 隐含绑定
谁调用this就指向谁。还有一种默认绑定在全局作用域调用会指向全局对象的,但是在严格模式下会返回undefined的。所以不归类。
function foo() {
console.log( this.a );
}
let obj = {
a: 2,
foo: foo
}
obj.foo(); // 2
- 明确绑定
利用call() apply() bind()
来强制this指向。
function foo() {
console.log( this.a );
}
let obj = {
a: 2
};
foo.call( obj ); // 2
===========================
function foo(something) {
console.log( this.a, something )
return this.a + something
}
let obj = {
a: 2
}
let bar = foo.bind( obj )
let b = bar( 3 ) // 2 3
console.log( b );// 5
bind(..)返回一个硬编码的新函数,它使用你指定的this环境来调用原本的函数。
- new绑定
this指向new构造调用函数而返回的新对象
function foo(a) {
this.a = a
}
let bar = new foo( 2 )
console.log( bar.a ) // 2
- super绑定
这个指向当前对象的原型对象,也就是_proro_
链上一层对象。
下面代码可以这样理解super.a()
等于foo.a.call(this)
,super.x
等于foo.x
let foo = {
x: 1,
a() {
return this.x
}
}
let bar = {
x: 2,
b() {
return `super.a()结果是${super.a()},super.x结果是${super.x}`
}
}
Object.setPrototypeOf(bar,foo)
bar.b() //? super.a()结果是2,super.x结果是1
- 箭头函数
箭头函数没有自己的this,只会从自己的作用域链的上一层继承this,并且固化无法再更改。也就说,this遵循的是词法作用域规则,而不是调用点绑定规则。
let foo = {
x:1,
a(){
return () => this.x //直接写无法定义,只能用返回值的方法。
},
b() {
return this.x
}
}
let bar = {
x: 2
}
let c = foo.a()
c() //? 1
foo.b() //? 1
foo.b.call(bar) //? 2
c.call(bar) //? 1无法改变
总结一下就是,this的调用取决于调用点,call等可以强制改变this,箭头函数会固化词法作用域this,new会指向创建的新对象,super则指向对象的原型对象。