本来只想简单的记录一下书中知识点,但是写着写着发现this和new书中没有说的很详细。 毕竟这本书的名字叫做《你不知道的Javascript》,而不是《JavaScript高级程序设计》(第四版),所以等后面再写个更详细的。
this
this在js中还是挺神奇的一个东西。
只要记住两点就好了:
-
不要把this用作查找作用域。
-
this指向的对象只取决于函数的调用方式。是我调用的,this就指向我,是你调用的this就指向你。
在实际代码执行的过程中,函数调用会被记录在调用栈中,this就是这个记录的一个属性。
this的绑定规则有四条:默认绑定、隐式绑定、显示绑定、new绑定。
默认绑定:
举个简单的例子来说:
1var a = 2;2function foo(){3 console.log(this) // Window, this指向window4 console.log(this.a) // 25}6foo()
注意: 严格模式下的默认绑定规则,会把this绑定在undefined上。
隐式绑定:
1function foo(){2 console.log(this) // {a:2, foo:f }, this指向obj3 console.log(this.a) // 24}5var obj = {6 a:2,7 foo,8}9
10obj.foo()
显示绑定
JavaScript提供的绝大多数函数以及你自己创建的所有函数都可以使用call(..)和apply(..)方法来绑定this。
常见的例子:
1function foo(){2 console.log(this.a)3}4var obj = {5 a:26}7foo.call(obj) // 2
- 硬绑定,绑定完成之后就无法再修改了
1function foo(){2 console.log(this.a)3}4var obj = {5 a:26}7var bar = function (){8 foo.call(obj)9}10bar() // 211setTimeout(bar , 100) // 212bar.call(window) // 2, 如果成功绑定window的话这个地方应该打印出undefined
其实在js中已经提供了这种硬绑定的方法就是使用bind
方法:
1function foo(something){2 console.log(this.a, something)3 return this.a + something4}5var obj = {6 a:27}8var bar = foo.bind(obj)9var b = bar(3) // 2 310console.log(b) // 5
- API调用的“上下文”
第三方库的许多函数,以及JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和bind(..)一样,确保你的回调函数使用指定的this。
1function foo(el) {2 console.log(el, this.id)3}4
5var 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来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
- 创建(或者说构造)一个全新的对象。
- 这个新对象会被执行[[Prototype]]连接。
- 这个新对象会绑定到函数调用的this。
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
1function foo(a){2 this.a = a3}4var bar = new foo(2)5console.log(bar()) // 2
箭头函数
箭头函数不使用this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this。
箭头函数可以像bind(..)一样确保函数的this被绑定到指定对象。
1function foo(){2 return (a) => {3 console.log(this.a)4 }5}6var obj1 = {a:2}7var obj2 = {a:3}8var bar = foo.call(obj1)9bar.call(obj2) // 2
foo()内部创建的箭头函数会捕获调用时foo()的this。
由于foo()的this绑定到obj1, bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改。(new也不行!)
对象
定义对象的方式
1// 方式一2var myObj = {3 key: value4}5
6// 方式二:7var myObj1 = new Object()8myObj.key = value
类型
属性访问
对象中的key只能是字符串,即使你使用了数字作为键,js还是会转换为字符串。
1obj = {2: 2}2console.log(obj) // obj:{"2": 2}
方法
函数永远不会“属于”某个对象。
1var myObj = {2 foo: function (){3 console.log("foo")4 }5}6var someFoo =myObj.foo()7someFoo() // function foo(){}8myObj.foo() // function foo(){}
即使你在对象的文字形式中声明一个函数表达式,这个函数也不会“属于”这个对象——它们只是对于相同函数对象的多个引用。
数组
数组也是对象,所以虽然每个下标都是整数,你仍然可以给数组添加属性:
1var arr = [1,2,3]2arr["bar"] = "bar"3console.log(arr.length) // 3, 这里的长度还是34console.log(arr) // [1,2,3,bar:"bar"]
注意:如果你给的键是数字那就会改变数组。
1var arr = [1,2,3]2arr[5] = "bar" // arr["5"] = “bar”,效果一样,属性名只能是字符串3console.log(arr.length) // 64console.log(arr) // [1, 2, 3, 空 ×2, 'bar']
浅复制、深复制
js中对所有对象的都是引用,不是复制。具体的内容可以查看我以前写的文章。
最简单的深拷贝实现方式:
1var cloneObj = JSON.parse(JSON.stringify(obj))
属性描述符
这是个大部分人都不会接触也不会用到的内容,但确实是个很重要的东西,在前端重要的响应中就有非常重要的作用。
1var myObject = {2 a:23}4// 获取属性描述符5Object.getOwnPropertyDescriptor(myObject, "a");6// 除了返回了自身的value,还返回了三个特性:writable(可写)、enumerable(可枚举)和configurable(可配置)。7//{value: 2, writable: true, enumerable: true, configurable: true}
1var obj = {2 a: 2,3 b: 44}5Object.defineProperty(obj, "a" , {6 value: 3,7 enumerable: false,8})9Object.keys(obj) // ["b"]
getter、setter
1const obj = {2 _propertyName: null3};4
5Object.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方法设置属性值22obj.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()方法来遍历所有返回值。
1var myArr = [1,2]2// 在[[Prototype]]链中会有一个迭代器方法:Symbol.iterator,所以数组可以使用for...of3console.log(myArr)4
5var myObj = {a: 1, b: 2}6
7for(let key of myArr){8 console.log(key)9}10
11var it = myArr[Symbol.iterator]()12it.next() // {value: 1, done: false}13
14// VM704:12 Uncaught TypeError: myObj is not iterable15for(let key of myObj){2 collapsed lines
16 console.log(key)17}
类
定义类
首先明确一点,其他语言中的类和JavaScript中的“类”并不一样。类是一种设计模式,JavaScript实现了近似类的功能。
js中使用class
来定义一个类,类可以理解为是一张建造图纸,用图纸造出来的房子就是实例。
一张图纸可以建造出来无数个房子,但是每个房子都是无法区分的,如果你想给房子加扇窗换个门,可以在实例化的时候调用构造函数,来给这个房子做一些变化。
js中把图纸变成房子的行为需要使用操作符new
。
1// 定义一个简单的JavaScript类2class 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
16const person1 = new Person('John', 25);17const person2 = new Person('Jane', 30);18
19// 调用实例方法20person1.sayHello(); // Hello, my name is John and I am 25 years old.21person2.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// 创建一个对象 obj12const obj1 = { prop: 'I am from obj1' };3
4// 使用 Object.create() 创建一个新对象 obj2,其原型为 obj15const obj2 = Object.create(obj1);6
7// 访问 obj2 的属性,如果在 obj2 中找不到,就会沿着原型链访问 obj1 中的属性8console.log(obj2.prop); // 输出: I am from obj1
ok,那接下来就有一些好玩的事情了:
- 在obj2的原型链上已经有了a属性,这时候在obj2上定义a的时候会创建一个屏蔽属性a,不会直接访问到原型链上的a。
1var obj = {2 a: 13}4var obj2 = Object.create(obj)5obj2.a = 26console.log(obj2) // {a: 2}7Object.getOwnPropertyDescriptor(obj,"a") // {value: 1, writable: true, enumerable: true, configurable: true}
- 如果obj的a属性是writable: false,将会忽略在obj2上的赋值操作,严格模式下会报错。
1Object.defineProperty(obj, "a", {2 value: 1,3 writable: false,4})5var obj2 = Object.create(obj)6obj2.a = 27console.log(obj2) // {}8Object.getOwnPropertyDescriptor(obj,"a") // {value: 1, writable: false, enumerable: true, configurable: true}
- 如果上一级的原型链上使用了setter,那么obj2上的a不会被添加到obj上但是会调用setter方法。
1var obj = {2 _a : '',3}4Object.defineProperty(obj, "a", {5 get:function (){6 return this._a * 27 },8 set: function (val){9 this._a = val10 }11})12obj.a = 213console.log(obj)14
15let obj2 = Object.create(obj)2 collapsed lines
16obj2.a = 417console.log(obj2)
如果你能看懂为什么这个obj.a 为什么打印出来是8的话,那还挺牛的。看不懂的话,那就是我的代码写的不好,嘿嘿。
解释一下: 这个get方法中的this虽然写在了obj中,但是在obj2中打印的时候这个this是指向obj2的。你给get和set方法上加上console.log就知道这个调用的时候是怎么回事了。
同样也可以看到,set方法在obj2赋值的时候也执行了。
1Object.defineProperty(obj, "a", {2 get:function (){3 console.log('get',this,this._a)4 return this._a * 25 },6 set: function (val){7 console.log('set',val)8 this._a = val9 }10})
如果你希望在第二种和第三种情况下也屏蔽a属性,那就不能使用=操作符来赋值,而是使用Object.defineProperty(..)来向obj中添加a。
隐式屏蔽
1let obj = {2 a: 23}4let obj2 = Object.create(obj)5obj2.a ++6console.log(obj2) // {a: 3}
事件委托
内容太多,记不下来了。。。
看了很多的书之后,发现很多知识并不是你单拎出来一个就能讲明白的,就好比这个原型和原型链。
单看这个知识点看个几遍,就算再绕也能理个差不多,但是仔细想想,那new关键字是干嘛的,Object.create()又是干嘛的,他们都能继承原型链,又怎么区分。
prototype和construct又是什么关系,又比如构造函数是什么,原型链有什么用,等等问题。
这就像十根耳机线缠在一起了,你捋一根线,捋个头出来其实没什么用,因为你再往后面捋就会发现还是一团乱麻。
就像看到了一个果子,找到树枝子,找到树干子,再到树根扎在土里,这个果子才算找到了头,不然这个果子一直悬在一个你说不清的地方。
所以关于一些复杂的点,得写的大一点,写的再多一点。