js 深度学习 -this 与 对象

js 深度学习 -this 与 对象

You-Dont-Know-JS(你不知道的 js 这本书的开源版本)

github 国内翻译

https://github.com/JoeHetfield/You-Dont-Know-JS

掘金中文

https://juejin.cn/post/6844903478813261831

github 原帖

https://github.com/getify/You-Dont-Know-JS

this

bind()  实现硬绑定 是一个如此常用的模式,它已作为 ES5 的内建工具提供:Function.prototype.bind

其本质实际是封装过的 call apply  的函数。只有在调用时才执行。实现 this 的直接锁定,不会被修改。

obj 调用 foo 可以理解为 obj 对象中 有个 foo 方法。就像 obj.foo() 这样调用,放轻松,不用死钻牛角,很简单的。

forEach fillter map  等数组方法的第二个参数都可以改变   该方法中第一个回调函数的 this 指向。如果没有第二个参数   默认 this  指向 undefined。框架开发中一般使用箭头函数,就几乎用不到第二个值了。

注意   普通函数中的 this 指向该函数的调用者

箭头函数是没有 this 的   它会向上一层一层去找。

new  改变 this 指向

当在函数前面被加入new调用时,也就是构造器调用时,下面这些事情会自动完成:

  1. 一个全新的对象会凭空创建(就是被构建)
  2. 这个新构建的对象会被接入原形链(**[[Prototype]]**-linked)
  3. 这个新构建的对象被设置为函数调用的this绑定
  4. 除非函数返回一个它自己的其他 对象,这个被new调用的函数将 自动 返回这个新构建的对象。

判定 this

从高到低 this 判定顺序依次为 — 高优先的 this 绑定会覆盖低级的 this 绑定

  1. 函数是和new一起被调用的吗(new 绑定)?如果是,this就是新构建的对象。
    var bar = new foo()
  2. 函数是用callapply被调用(明确绑定),甚至是隐藏在bind 硬绑定 之中吗?如果是,this就是明确指定的对象。
    var bar = foo.call( obj2 )
  3. 函数是用环境对象(也称为拥有者或容器对象)被调用的吗(隐含绑定)?如果是,this就是那个环境对象。
    var bar = obj1.foo()
  4. 否则,使用默认的this默认绑定)。如果在strict mode下,就是undefined,否则是global(window)对象。 var bar = foo()

以上,就是理解对于普通的函数调用来说的this绑定规则所需的全部。是的···几乎是全部。

小心偶然或不经意的 默认绑定 规则调用。如果你想“安全”地忽略this绑定,一个像ø = Object.create(null)这样的“DMZ”对象是一个很好的占位值,来保护global对象不受意外的副作用影响。


对象

.a语法通常称为“属性(property)”访问 [“a”]语法通常称为“键(key)”访问。在现实中,它们俩都访问相同的 位置,而且会拿出相同的值,

<font style="color:#333333;">.</font>操作符后面需要一个标识符(Identifier)兼容的属性名,而[“..”]语法基本可以接收任何兼容 UTF-8/unicode 的字符串作为属性名。举个例子,为了引用一个名为“Super-Fun!”的属性,你不得不使用[“Super-Fun!”]语法访问,因为Super-Fun!不是一个合法的Identifier属性名。[‘xx’]可以引入不合法的标识符。

由于[“..”]语法使用字符串的 来指定位置,这意味着程序可以**动****态**地组建字符串的值。

计算表达式 使用myObject[..] 比如myObject[prefix + name]

函数以及方法   从来不属于任何对象的属性,它只是被单纯调用而已

数组

也是一种对象  

伪数组 转 真数组

1
2
[].slice.call(伪数组); // 老方法
Array.from(伪数组); // es6

可以转化 arguments,元素集合,还可以转化类数组对象,为真正的数组,使用数组的方法.

(不推荐的做法) 即使索引每条都是正整数**  依旧可以添加属性**

这种   键值对的添加方式   并不会改变 length,  一般都不会这样使用   数组对象

属性描述符(Property Descriptors)

开始深入对象底层啦

Object.getOwnPropertyDescriptor( 对象名, 属性值 )

**  可以获取对象某属性的具体描述信息**

Object.defineProperty( 对象名, 属性名,{ 配置描述符 } )

则可以配置   对象的描述信息,vue2 的响应原理就是利用这个完成的


配置信息详解

1. 可写性(Writable)

writable控制着你改变属性值的能力。

考虑这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
var myObject = {};

Object.defineProperty(myObject, "a", {
value: 2,
writable: false, // 不可写!
configurable: true,
enumerable: true,
});

myObject.a = 3;

myObject.a; // 2

如你所见,我们对value的修改悄无声息地失败了。如果我们在strict mode下进行尝试,会得到一个错误:

1
2
3
4
5
6
7
8
9
10
11
12
"use strict";

var myObject = {};

Object.defineProperty(myObject, "a", {
value: 2,
writable: false, // not writable!
configurable: true,
enumerable: true,
});

myObject.a = 3; // TypeError

这个TypeError告诉我们,我们不能改变一个不可写属性。

注意: 我们一会儿就会讨论 getters/setters,但是简单地说,你可以观察到writable:false意味着值不可改变,和你定义一个空的 setter 是有些等价的。实际上,你的空 setter 在被调用时需要扔出一个TypeError,来和writable:false保持一致。

2. 可配置性(Configurable)

只要属性当前是可配置的,我们就可以使用同样的defineProperty(..)工具,修改它的描述符定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var myObject = {
a: 2,
};

myObject.a = 3;
myObject.a; // 3

Object.defineProperty(myObject, "a", {
value: 4,
writable: true,
configurable: false, // 不可配置!
enumerable: true,
});

myObject.a; // 4
myObject.a = 5;
myObject.a; // 5

Object.defineProperty(myObject, "a", {
value: 6,
writable: true,
configurable: true,
enumerable: true,
}); // TypeError

最后的defineProperty(..)调用导致了一个 TypeError,这与strict mode无关,如果你试图改变一个不可配置属性的描述符定义,就会发生 TypeError。要小心:如你所看到的,将configurable设置为false **一个单向操作,不可撤销****!**

注意: 这里有一个需要注意的微小例外:即便属性已经是configurable:falsewritable总是可以没有错误地从true改变为false,但如果已经是false的话不能变回true

configurable:false阻止的另外一个事情是使用delete操作符移除既存属性的能力。

就是配置了 configurable:false 后 delete 会无效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var myObject = {
a: 2,
};

myObject.a; // 2
delete myObject.a;
myObject.a; // undefined

Object.defineProperty(myObject, "a", {
value: 2,
writable: true,
configurable: false,
enumerable: true,
});

myObject.a; // 2
delete myObject.a;
myObject.a; // 2

如你所见,最后的delete调用失败了(无声地),因为我们将a属性设置成了不可配置。

**delete****仅用于直接从目标对象移除该对象的属性(可以被移除的属性**)。如果一个对象的属性是某个其他对象/函数的最后一个现存的引用,而你delete了它,那么这就移除了这个引用,于是现在那个没有被任何地方引用的对象/函数就可以被作为垃圾回收。

3. 可枚举性(Enumerable)

这个一直以来非常的打我脑壳,其实完全就可以理解为是否可以循环和遍历的。

如果 a 属性的 enumerable 在属性描述符配置中设置为 false,

那么在循环遍历中,a 属性将会被隐藏,不会遍历和迭代代。

但是不可枚举的对象属性,却依旧是存在该对象属性上的。

可以被

**in ** (该方法会遍历从原型链上继承到的属性

hasOwnProperty ( 该方法会忽略掉那些从原型链上继承到的属性

检测到的

propertyIsEnumerable  可以判断   属性是否具有枚举性

1
2
myObject.propertyIsEnumerable("a"); // true
myObject.propertyIsEnumerable("b"); // false

for in  和 Object.keys  都无法遍历不可枚举的属性

但是  Object.getOwnPropertyNames  可以遍历出 **不可枚举****的属性 !!  破案了**

它的名称可能已经使它的功能很明显了,这个性质控制着一个属性是否能在特定的对象属性枚举操作中出现,比如for..in循环。设置为false将会阻止它出现在这样的枚举中,即使它依然完全是可以访问的。设置为true会使它出现。

所有普通的用户定义属性都默认是可enumerable的,正如你通常希望的那样。但如果你有一个特殊的属性,你想让它对枚举隐藏,就将它设置为enumerable:false

不可变性(Immutability)

一个重要的注意点是:所有 这些方法都创建的是浅不可变性(参考浅拷贝)。也就是,它们仅影响对象和它的直属属性的性质。如果对象拥有对其他对象(数组,对象,函数等)的引用,那个对象的 内容 不会受影响,任然保持可变。

1
2
3
myImmutableObject.foo; // [1,2,3]
myImmutableObject.foo.push(4);
myImmutableObject.foo; // [1,2,3,4]

在这段代码中,我们假想myImmutableObject已经被创建,而且被保护为不可变。但是,为了保护myImmutableObject.foo的内容(也是一个对象——数组),你将需要使用下面的一个或多个方法将foo设置为Immutability

注意: 在 JS 程序中创建完全不可动摇的对象是不那么常见的。有些特殊情况当然需要,但作为一个普通的设计模式,如果你发现自己想要 封印(seal) 冻结(freeze) 你所有的对象,那么你可能想要退一步来重新考虑你的程序设计,让它对对象值的潜在变化更加健壮。

1. 对象常量(Object Constant)

通过将writable:falseconfigurable:false组合,你可以实质上创建了一个作为对象属性的 常量**(不能被改变,重定义或删除)**,比如:

注意并不是 const 定义的 甚至比 const 更严格 但是没有 const 灵活

1
2
3
4
5
6
7
var myObject = {};

Object.defineProperty(myObject, "FAVORITE_NUMBER", {
value: 42,
writable: false,
configurable: false,
});

2. 防止扩展(Prevent Extensions)

如果你想防止一个对象被添加新的属性,但另一方面保留其他既存的对象属性,调用Object.preventExtensions(..)

1
2
3
4
5
6
7
8
var myObject = {
a: 2,
};

Object.preventExtensions(myObject);

myObject.b = 3;
myObject.b; // undefined

非-strict mode模式下,b的创建会无声地失败。在strict mode下,它会抛出TypeError

3. 封印(Seal)

Object.seal(..)创建一个“封印”的对象,这意味着它实质上在当前的对象上调用**Object.preventExtensions(..)并且同时**也将它所有的既存属性标记为**configurable:false**。

所以,你既不能添加更多的属性,也不能重新配置或删除既存属性(就是不能使用 Object.defineProperty了)(虽然你依然 可以 修改它们的值)。

4. 冻结(Freeze)

加强版的 seal 封印,就连值都无法被修改了。

Object.freeze(..)创建一个冻结的对象,这意味着它实质上在当前的对象上调用Object.seal(..),同时也将它所有的“数据访问”属性设置为writable:false,所以他们的值不可改变。

你可以“深度冻结”一个对象:在这个对象上调用Object.freeze(..),然后递归地迭代所有它引用的对象(目前还没有受过影响的),然后在它们上也调用Object.freeze(..)。但是要小心,这可能会影响其他(共享的)你并不打算影响的对象。

[[Get]] 与 [[Put]]

就是字面意思 可以把这两个理解为函数,就是当在获取对象值或修改对象值时,会触发的隐藏机制。


[[Get]]

对一个对象进行默认的内建[[Get]]操作,会 首先 检查对象,寻找一个拥有被请求的名称的属性,如果找到,就返回相应的值。

然而,如果按照被请求的名称 没能 找到属性,[[Get]]的算法定义了另一个重要的行为。就是遍历[[Prototype]]链,如果有的话)。 ( prototype 链 是 [[Get]] 算法的一部分

[[Get]]操作的一个重要结果是,如果它通过任何方法都不能找到被请求的属性的值,那么它会返回undefined

所有看似 理所应当的一个结果 其实经历了这么多的处理 QAQ

[[Put]]

  1. 这个属性是 访问器描述符(getter setter) 如果是,而且是 setter,就调用 setter。
  2. 这个属性是writablefalse数据描述符?如果是,在非**strict mode**下无声地失败,或者在**strict mode**下抛出**TypeError**。
  3. 否则,像平常一样设置既存属性的值。

Getters 与 Setters

Getter 是实际上调用一个隐藏函数来取得值的属性。Setter 是实际上调用一个隐藏函数来设置值的属性。

当你将一个属性定义为拥有 getter 或 setter 或两者兼备,那么它的定义就成为了“访问器描述符”。对于访问器描述符,它的valuewritable性质没有意义而被忽略,取而代之的是 JS 将会考虑属性的setget性质(还有configurableenumerable)。

对象语法中使用get a() { .. },还是通过使用defineProperty(..)明确定义,我们都在对象上创建了一个没有实际持有值的属性访问它们 将会自动地对 getter 函数进行隐藏的函数调用,其返回的任何值就是属性访问的结果。

对 getter 修饰的属性 进行赋值操作是没有意义的。是无效的。

如果你想   对其进行类似 [[put]]  操作   需要用到 setter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var myObject = {
// 为`a`定义getter
get a() {
return this._a_;
},

// 为`a`定义setter
set a(val) {
this._a_ = val * 2;
},
};

myObject.a = 2; // setter 操作覆盖了 原本的 [[put]] 操作

myObject.a; // 4

存在性(Existence)

我们早先看到,像myObject.a这样的属性访问可能会得到一个undefined值,无论是它明确存储着undefined还是属性a根本就不存在。那么,如果这两种情况的值相同,我们还怎么区别它们呢?

我们可以查询一个对象是否拥有特定的属性,而不必取得那个属性的值:

1
2
3
4
5
6
7
8
9
var myObject = {
a: 2,
};

"a" in myObject; // true
"b" in myObject; // false

myObject.hasOwnProperty("a"); // true
myObject.hasOwnProperty("b"); // false

in操作符会检查属性是否存在于对象 ,或者是否存在于[[Prototype]]链对象遍历的更高层中。相比之下,hasOwnProperty(..) 仅仅 检查myObject是否拥有属性,但 不会 查询[[Prototype]]链。

通过委托到Object.prototype,所有的普通对象都可以访问hasOwnProperty(..)(详见第五章)。但是创建一个不链接到Object.prototype的对象也是可能的。这种情况下,像myObject.hasOwnProperty(..)这样的方法调用将会失败。

在这种场景下,一个进行这种检查的更健壮的方式是Object.prototype.hasOwnProperty.call(myObject,”a”),它借用基本的hasOwnProperty(..)方法而且使用 明确的this绑定来对我们的myObject实施这个方法。

注意: in操作符看起来像是要检查一个值在容器中的存在性,但是它实际上检查的是属性名的存在性。在使用数组时注意这个区别十分重要,因为我们会有很强的冲动来进行4 in [2, 4, 6]这样的检查,但是这总是不像我们想象的那样工作。

关于 for…of

你也可以使用 ES6 的for..of语法,在数据结构(数组,对象等)中迭代 ,它寻找一个内建或自定义的@@iterator对象,这个对象由一个next()方法组成,通过这个next()方法每次迭代一个数据。

只有数组才有 自建的**@@iterator 对象**。 普通对象想使用 for…of 只有自建一个**@@iterator 对象**


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!