Cirry's Blog

《你不知道的Javascript》上卷第二部分(总结)

2024-03-01
阅读
最后更新:2024-03-26
15分钟
2847字

本来只想简单的记录一下书中知识点,但是写着写着发现this和new书中没有说的很详细。 毕竟这本书的名字叫做《你不知道的Javascript》,而不是《JavaScript高级程序设计》(第四版),所以等后面再写个更详细的。

this

this在js中还是挺神奇的一个东西。

只要记住两点就好了:

  • 不要把this用作查找作用域。

  • this指向的对象只取决于函数的调用方式。是我调用的,this就指向我,是你调用的this就指向你。

在实际代码执行的过程中,函数调用会被记录在调用栈中,this就是这个记录的一个属性。

this的绑定规则有四条:默认绑定、隐式绑定、显示绑定、new绑定。

默认绑定:

举个简单的例子来说:

1
var a = 2;
2
function foo(){
3
console.log(this) // Window, this指向window
4
console.log(this.a) // 2
5
}
6
foo()

注意: 严格模式下的默认绑定规则,会把this绑定在undefined上。

隐式绑定:

1
function foo(){
2
console.log(this) // {a:2, foo:f }, this指向obj
3
console.log(this.a) // 2
4
}
5
var obj = {
6
a:2,
7
foo,
8
}
9
10
obj.foo()

显示绑定

JavaScript提供的绝大多数函数以及你自己创建的所有函数都可以使用call(..)和apply(..)方法来绑定this。

常见的例子:

1
function foo(){
2
console.log(this.a)
3
}
4
var obj = {
5
a:2
6
}
7
foo.call(obj) // 2
  • 硬绑定,绑定完成之后就无法再修改了
1
function foo(){
2
console.log(this.a)
3
}
4
var obj = {
5
a:2
6
}
7
var bar = function (){
8
foo.call(obj)
9
}
10
bar() // 2
11
setTimeout(bar , 100) // 2
12
bar.call(window) // 2, 如果成功绑定window的话这个地方应该打印出undefined

其实在js中已经提供了这种硬绑定的方法就是使用bind方法:

1
function foo(something){
2
console.log(this.a, something)
3
return this.a + something
4
}
5
var obj = {
6
a:2
7
}
8
var bar = foo.bind(obj)
9
var b = bar(3) // 2 3
10
console.log(b) // 5
  • API调用的“上下文”

第三方库的许多函数,以及JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和bind(..)一样,确保你的回调函数使用指定的this。

1
function foo(el) {
2
console.log(el, this.id)
3
}
4
5
var obj = {
6
id: "awesome"
7
}
8
// 调用foo时把this绑定到obj上
9
[1,2,3].forEach(foo, obj) // 1 awesome 2 awesome 3 awesome

new绑定

不要把js中的new想象成其他语言中的new,你可以简单的理解为使用new来调用一个普通的js函数。

而当你使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行[[Prototype]]连接。
  3. 这个新对象会绑定到函数调用的this。
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
1
function foo(a){
2
this.a = a
3
}
4
var bar = new foo(2)
5
console.log(bar()) // 2

箭头函数

箭头函数不使用this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this。

箭头函数可以像bind(..)一样确保函数的this被绑定到指定对象。

1
function foo(){
2
return (a) => {
3
console.log(this.a)
4
}
5
}
6
var obj1 = {a:2}
7
var obj2 = {a:3}
8
var bar = foo.call(obj1)
9
bar.call(obj2) // 2

foo()内部创建的箭头函数会捕获调用时foo()的this。

由于foo()的this绑定到obj1, bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改。(new也不行!)

对象

定义对象的方式

1
// 方式一
2
var myObj = {
3
key: value
4
}
5
6
// 方式二:
7
var myObj1 = new Object()
8
myObj.key = value

类型

属性访问

对象中的key只能是字符串,即使你使用了数字作为键,js还是会转换为字符串。

1
obj = {2: 2}
2
console.log(obj) // obj:{"2": 2}

方法

函数永远不会“属于”某个对象。

1
var myObj = {
2
foo: function (){
3
console.log("foo")
4
}
5
}
6
var someFoo =myObj.foo()
7
someFoo() // function foo(){}
8
myObj.foo() // function foo(){}

即使你在对象的文字形式中声明一个函数表达式,这个函数也不会“属于”这个对象——它们只是对于相同函数对象的多个引用。

数组

数组也是对象,所以虽然每个下标都是整数,你仍然可以给数组添加属性:

1
var arr = [1,2,3]
2
arr["bar"] = "bar"
3
console.log(arr.length) // 3, 这里的长度还是3
4
console.log(arr) // [1,2,3,bar:"bar"]

注意:如果你给的键是数字那就会改变数组。

1
var arr = [1,2,3]
2
arr[5] = "bar" // arr["5"] = “bar”,效果一样,属性名只能是字符串
3
console.log(arr.length) // 6
4
console.log(arr) // [1, 2, 3, 空 ×2, 'bar']

浅复制、深复制

js中对所有对象的都是引用,不是复制。具体的内容可以查看我以前写的文章。

js实现深拷贝(深度克隆)

最简单的深拷贝实现方式:

1
var cloneObj = JSON.parse(JSON.stringify(obj))

属性描述符

这是个大部分人都不会接触也不会用到的内容,但确实是个很重要的东西,在前端重要的响应中就有非常重要的作用。

1
var myObject = {
2
a:2
3
}
4
// 获取属性描述符
5
Object.getOwnPropertyDescriptor(myObject, "a");
6
// 除了返回了自身的value,还返回了三个特性:writable(可写)、enumerable(可枚举)和configurable(可配置)。
7
//{value: 2, writable: true, enumerable: true, configurable: true}
1
var obj = {
2
a: 2,
3
b: 4
4
}
5
Object.defineProperty(obj, "a" , {
6
value: 3,
7
enumerable: false,
8
})
9
Object.keys(obj) // ["b"]

getter、setter

1
const obj = {
2
_propertyName: null
3
};
4
5
Object.defineProperty(obj, 'propertyName', {
6
// 定义getter方法
7
get: function() {
8
return this._propertyName;
9
},
10
11
// 定义setter方法
12
set: function(value) {
13
// 在这里可以执行额外的逻辑
14
console.log(`Setting the property to: ${value}`);
15
7 collapsed lines
16
// 将值赋给属性
17
this._propertyName = value;
18
}
19
});
20
21
// 使用setter方法设置属性值
22
obj.propertyName = "New Value";

Object.keys(..)会返回一个数组,包含所有可枚举属性,Object.getOwnPropertyNames(..)会返回一个数组,包含所有属性,无论它们是否可枚举。

in和hasOwnProperty(..)的区别在于是否查找[[Prototype]]链,然而,Object.keys(..)和Object.getOwnPropertyNames(..)都只会查找对象直接包含的属性。

for…in/for…of

for..in循环可以用来遍历对象的可枚举属性列表。

for…of是用来遍历数组的语法,for..of循环首先会向被访问对象请求一个迭代器对象,然后通过调用迭代器对象的next()方法来遍历所有返回值。

1
var myArr = [1,2]
2
// 在[[Prototype]]链中会有一个迭代器方法:Symbol.iterator,所以数组可以使用for...of
3
console.log(myArr)
4
5
var myObj = {a: 1, b: 2}
6
7
for(let key of myArr){
8
console.log(key)
9
}
10
11
var it = myArr[Symbol.iterator]()
12
it.next() // {value: 1, done: false}
13
14
// VM704:12 Uncaught TypeError: myObj is not iterable
15
for(let key of myObj){
2 collapsed lines
16
console.log(key)
17
}

定义类

首先明确一点,其他语言中的类和JavaScript中的“类”并不一样。类是一种设计模式,JavaScript实现了近似类的功能。

js中使用class来定义一个类,类可以理解为是一张建造图纸,用图纸造出来的房子就是实例。

一张图纸可以建造出来无数个房子,但是每个房子都是无法区分的,如果你想给房子加扇窗换个门,可以在实例化的时候调用构造函数,来给这个房子做一些变化。

js中把图纸变成房子的行为需要使用操作符new

1
// 定义一个简单的JavaScript类
2
class Person {
3
// 构造函数
4
constructor(name, age) {
5
this.name = name;
6
this.age = age;
7
}
8
9
// 实例方法
10
sayHello() {
11
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
12
}
13
}
14
15
// 创建类的实例
6 collapsed lines
16
const person1 = new Person('John', 25);
17
const person2 = new Person('Jane', 30);
18
19
// 调用实例方法
20
person1.sayHello(); // Hello, my name is John and I am 25 years old.
21
person2.sayHello(); // Hello, my name is Jane and I am 30 years old.

类的内容太多了,感觉这一部分书中讲的不是很好。后面我在补充补充吧。

原型、原型链

关于原型和原型链是什么,可以看我直接写的文章深入理解(图解)js中的原型,原型对象,原型链

Object.create() 静态方法以一个现有对象作为原型,创建一个新对象,并继承原型链。

题外话:所以如果你想创建一个空对象,并不是let obj={},这个时候的obj会有toString()等Object原型链上的方法。而使用let obj = Object.create(null),你会得到一个干净的空对象。

1
// 创建一个对象 obj1
2
const obj1 = { prop: 'I am from obj1' };
3
4
// 使用 Object.create() 创建一个新对象 obj2,其原型为 obj1
5
const obj2 = Object.create(obj1);
6
7
// 访问 obj2 的属性,如果在 obj2 中找不到,就会沿着原型链访问 obj1 中的属性
8
console.log(obj2.prop); // 输出: I am from obj1

ok,那接下来就有一些好玩的事情了:

  1. 在obj2的原型链上已经有了a属性,这时候在obj2上定义a的时候会创建一个屏蔽属性a,不会直接访问到原型链上的a。
1
var obj = {
2
a: 1
3
}
4
var obj2 = Object.create(obj)
5
obj2.a = 2
6
console.log(obj2) // {a: 2}
7
Object.getOwnPropertyDescriptor(obj,"a") // {value: 1, writable: true, enumerable: true, configurable: true}
  1. 如果obj的a属性是writable: false,将会忽略在obj2上的赋值操作,严格模式下会报错。
1
Object.defineProperty(obj, "a", {
2
value: 1,
3
writable: false,
4
})
5
var obj2 = Object.create(obj)
6
obj2.a = 2
7
console.log(obj2) // {}
8
Object.getOwnPropertyDescriptor(obj,"a") // {value: 1, writable: false, enumerable: true, configurable: true}
  1. 如果上一级的原型链上使用了setter,那么obj2上的a不会被添加到obj上但是会调用setter方法。
1
var obj = {
2
_a : '',
3
}
4
Object.defineProperty(obj, "a", {
5
get:function (){
6
return this._a * 2
7
},
8
set: function (val){
9
this._a = val
10
}
11
})
12
obj.a = 2
13
console.log(obj)
14
15
let obj2 = Object.create(obj)
2 collapsed lines
16
obj2.a = 4
17
console.log(obj2)

如果你能看懂为什么这个obj.a 为什么打印出来是8的话,那还挺牛的。看不懂的话,那就是我的代码写的不好,嘿嘿。

解释一下: 这个get方法中的this虽然写在了obj中,但是在obj2中打印的时候这个this是指向obj2的。你给get和set方法上加上console.log就知道这个调用的时候是怎么回事了。

同样也可以看到,set方法在obj2赋值的时候也执行了。

1
Object.defineProperty(obj, "a", {
2
get:function (){
3
console.log('get',this,this._a)
4
return this._a * 2
5
},
6
set: function (val){
7
console.log('set',val)
8
this._a = val
9
}
10
})

如果你希望在第二种和第三种情况下也屏蔽a属性,那就不能使用=操作符来赋值,而是使用Object.defineProperty(..)来向obj中添加a。

隐式屏蔽

1
let obj = {
2
a: 2
3
}
4
let obj2 = Object.create(obj)
5
obj2.a ++
6
console.log(obj2) // {a: 3}

事件委托

内容太多,记不下来了。。。

看了很多的书之后,发现很多知识并不是你单拎出来一个就能讲明白的,就好比这个原型和原型链。

单看这个知识点看个几遍,就算再绕也能理个差不多,但是仔细想想,那new关键字是干嘛的,Object.create()又是干嘛的,他们都能继承原型链,又怎么区分。

prototype和construct又是什么关系,又比如构造函数是什么,原型链有什么用,等等问题。

这就像十根耳机线缠在一起了,你捋一根线,捋个头出来其实没什么用,因为你再往后面捋就会发现还是一团乱麻。

就像看到了一个果子,找到树枝子,找到树干子,再到树根扎在土里,这个果子才算找到了头,不然这个果子一直悬在一个你说不清的地方。

所以关于一些复杂的点,得写的大一点,写的再多一点。

本文标题:《你不知道的Javascript》上卷第二部分(总结)
文章作者:Cirry
发布时间:2024-03-01
版权声明:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
感谢大佬送来的咖啡☕
alipayQRCode
wechatQRCode