发布时间:2021年7月17日
该篇文章内容涉及《你不知道的JavaScript》上的第二部分 ———— this 和 对象原型
在JavaScript中,this常用来传递一个隐式的对象引用。如果我们不使用 this 进行引用传递时,往往需要手动传入上下文内容来保证我们的代码能够正常运行
例如:
const MyInformation = {
name: 'mocha'
}
// 不使用this
function outputMyName(context) {
console.log(`your name is ${context.name}`);
}
outputMyName(MyInformation);
// 使用this
function outputMyName() {
console.log(`your name is ${this.name}`);
}
outputMyName.call(MyInformation);
其实关于this的问题也是一个老生常谈的内容,通常来讲我们认为 在非强制修改 this 指向的情况下,函数中的 this 一般指向其调用方的上下文。这其中有一个比较重要的概念:调用方的上下文
为什么我们把函数执行说成函数调用?首先我们还是要回到最基础的 JavaScript 中,我们都知道 JS 中基本数据类型有 number/string/bool/object/undefined/null/symbol 这几种,而函数 Function 其实只是内置对象的一种,而对象在内存中都是存放在内存堆中,我们所有对函数执行其实都是对某一个内存地址的调用
所以我们就可以更清晰的给一个结论,在非强制修改 this 指向的情况下,指向函数内存地址的变量在哪里被执行(即函数被执行),函数的 this 就指向哪里
举个例子:
function outputMyName() {
console.log(`your name is ${this.name}`);
}
const MyInformation = {
name: 'mocha',
outputMyName: outputMyName
}
var name = '王亚辉';
outputMyName(); // your name is 王亚辉
MyInformation.outputMyName(); // your name is mocha;
在第一个 outputMyName() 中,outputName指向真正函数保存的内存地址,在 window 被调用,所以此时 this 指向全局 window
第二个 MyInformation.outputMyName() 中,MyInformation.outputMyName 这个变量指向函数在内存中的地址,而 MyInformation.outputMyName 是属于 MyInformation 这个对象的一个变量,所以当这个变量被执行时,this 会指向 MyInformation 这个对象
在 《你不知道的 JavaScript》这本书中,作者说:
this 在任何情况下都不指向函数的词法作用域
this 不可能在词法作用域中查找到什么
this 是在函数运行时进行绑定的,不是在函数声明式进行绑定的,它的上下文取决于被调用时的条件
关于词法作用域,我们上一篇文章已经讨论过,简单来说 词法作用域就是在函数定义时就已经确定的作用域
所以下面这个例子并不能打印出什么东西
function speak() {
var name = 'mocha';
outputMyName();
}
function outputMyName() {
console.log(`your name is ${this.name}`);
}
speak(); // your name is undefined.
在 speak 函数中,name 变量是我们在函数定义时就已经确定的,所以它是属于词法作用域的变量。因为 this 不可能在词法作用域中寻找到什么,所以我们在 outputMyName 函数中并不能打印出 name 变量的值
默认绑定是最基本的一种绑定方式,当函数在不带任何修饰符的情况下被调用时,就会应用默认绑定原则
function foo() {
console.log(this.name);
}
var name = 'mocha';
foo(); // mocha
当 foo 函数内部使用严格模式
use strict时,this.name 是无法访问到全局的 name 变量的。但当 foo 函数在严格模式下被调用时,是可以访问的。过于教条,仅供了解
隐式绑定其实也是我们经常会用到的,简单来说,当函数在某个上下文中被调用时,就会应用隐式绑定规则,将函数的 this 指向调用方的上下文。这个例子我们前面其实也说过了,可以再看一遍
function outputMyName() {
console.log(`your name is ${this.name}`);
}
const MyInformation = {
name: 'mocha',
outputMyName: outputMyName
}
// 函数在MyInformation中被调用,所以this指向MyInformation
MyInformation.outputMyName(); // your name is mocha;
先看例子:
function outputMyName() {
console.log(`your name is ${this.name}`);
}
const MyInformation = {
name: 'mocha',
outputMyName: outputMyName
}
var name = '王亚辉';
var output = MyInformation.outputMyName;
output(); // your name is 王亚辉;
在这本书中,作者将这种情况叫做隐式丢失,其实如果记得我们上面总结的内容,那么这个就很好理解
MyInformation.outputMyName 是一个指向 outputName 内存地址的变量output 这个变量,值与 MyInformation.outputMyName 相同,也同样指向 outputName 函数在内存中地址output 变量在 全局作用域 window 下被执行,也就是说 outputName 在 window 下被执行, 所以此时 this 指向 window其实就是通过 call 、bind、apply 来强制的修改 this 指向,不多叙述。唯一需要注意的是,bind 其实会创建一个新的函数,并把我们传递的 this 设置为这个新函数的 this,而且我们可以通过 bind 来传递一些默认的参数:
// age 是我们默认绑定的参数,在调用时传递的参数依次向后移动
function outputName(age, city) {
console.log(`my name is ${this.name}, i am ${age} years old, i am from ${city}`);
}
const MyInformation = {
name: 'mocha'
}
const output = outputName.bind(MyInformation, 26);
output('郑州'); // my name is mocha, i am 26 years old, i am from 郑州
在 JavaScript 中,当使用 new 操作符对一个函数进行 构造调用 时,会执行以下操作:
function New() {}New.prototype = Object.create(Old.prototype)var result = New.call(newThis)function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2
new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定
当我们在使用 call、apply、bind 对函数进行显式绑定时,如果传递给函数的 this 为 null/undefined ,那么传递的 this 会被忽略,在调用时采用默认绑定规则。
过于教条,没人会写以下这种代码,了解即可,无需在意
function foo() {
console.log(this.a);
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 }
// p.foo = o.foo 这句代码返回的值是foo函数的内存地址,等于在 window 下执行了foo,所以为2
(p.foo = o.foo)(); // 2
没有自己的 this,使用上一级的 this。由于其继承了其上一层的 this ,所以我们无需像以前一样关心箭头函数内部的可能存在的 this 指向错误问题,也可以是用词法作用域取代了传统的 this 机制。
基本类型:string 、number、 boolean、 null 、 undefined 、object
老生常谈:为什么
typeof null会返回object?答:在 JavaScript 中,数据地址以
00开头的均会被认为是object类型,而null地址全部为0,所以会被误认为是obejct类型,实际上是一种基本类型
内置对象:String、 Number、 Boolean、 Object、 Function、 Array、 Date、 RegExp、 Error
在变量声明方面, string、number、boolean、object 可以使用构造形式以及文字形式来声明; null、undefined 只有文字形式声明这一种方式;Date 只有构造形式没有文字形式声明方式
基本类型均为小写,内置的对象类型均为大写开头
Object.x 只适用于 key 只接受 UTF-8/Unicode 字符串类型,如果是复杂的 key,如 gyjames-mocha! 这种,必须采用 Object[x] 的方式取值。
在文字形式声明中,用 [] 包裹起来的计算表达式可以用作对象的 key,比较常见于将 symbol 类型用作 key 的场景
var nameKey = Symbol.name;
var myObject = {
[nameKey]: 'mocha'
}
myObject[nameKey]; // mocha
我们习惯将对象中除 function 外的变量称作它的 属性, 将 function 称作 方法。本质上他们都属于对变量的引用,都属于 属性。
数组是一组 key 为从 0-length 的特殊对象,对对象来说,我们也可以给数组赋值一个 string 类型的key,但是赋值后数组的长度并不会发生变化。
复制对象有两种方式
JSON.parse(JSON.stringify(object)),最简单的深拷贝Object.assign,当 key 对应的 value 为引用类型时,依然只会复制内存地址,并不会重新创建,本质和通过 = 赋值相同从 ES5 开始,对象中的每个属性都有了属性描述符。
属性描述符共包含几个特性:
true/falsetrue/falsewritable/enumerable 值,实际上当 configurable 为 false,即不可配置时,只是不允许把 writable/enumerable 的状态由 false 转成 true,即不可配置只对属性的开启有限制作用,属性状态由开启到关闭是允许的。而且对应的获取对象中某一个属性的描述符的方法为 Object.getOwnPropertyDescription,使用方式如下:
const myObject = {
a: 2
}
Object.getOwnPropertyDescription(myObject, 'a');
=>
{
value: 2,
configurable: true,
writable: true,
enumerable: true
}
了解过 Vue 数据响应式实现原理的应该都会了解过这个设置属性描述信息的方法,即 Object.defineProperty,使用方式如下:
const myObject = {};
Object.defineProperty(myObject, 'a', {
value: 2,
configurable: true,
writable: true,
enumerable: true
})
可枚举型代表着该属性是否可以在 iterator 方法(如 for ... in ...)中,是否可以访问到这个变量。对于属性的直接访问是不受影响的。
与该属性相关的一些方法有:
Object.keys => 该方法只能够获取到对象中 enumerable 为 true 的属性Object.getOwnPropertyNames => 该方法会返回所有的属性,无论是否可枚举通过对属性描述符的配置,我们可以实现一些功能
const myObject = {};
Object.defineProperty(myObject, 'CONSTANT_VALUE', {
value: 'mocha',
writable: false,
enumerable: false,
configurable: false
})
const myObject = {
name: 'mocha'
};
// 禁止新增属性
Object.preventExtensions(myObject);
myObject.age = 26;
console.log(myObject.age); // undefined
密封一个对方的方法为 Object.seal,该方法会在对象上调用 Object.preventExtensions 方法并且将所有的属性的 configurable 属性修改为 false,即无法新增也无法删除属性,但可修改属性的值
Object.freeze 为冻结一个对象的方法,在方法在 Object.seal 的基础上会增加一个将属性的 writable 属性修改为 false 的处理。即既无法新增、删除属性,也无法已有属性的值。该方法是可以应用在对象上的最高级的方法。
在前面我们说到了 Vue2 中实现数据绑定的方法就是用的订阅发布者模式 Object.defineProperty,那么只用以上四个特性是无法实现数据变动后立刻感知的,所以我们需要继续了解 [[Get]]/[[Put]] 以及 Getter/Setter
[[Get]] 和 [[Put]] 是对象中自带的两个操作。
当我们想要获取到对象某一个属性的值是,我们是通过 Oject.x / Object[x] 的方式去获取的,实际上 JavaScript 引擎会调用 [[Get]] 去查找对象中是否有这个属性,如果有就返回我们要查找的属性,没有的话会沿着原型链往上查找,直到查找到顶层的内置的 Object 对象,如果都没有这个属性,就返回 undefined
当我们对一个属性值进行设置的时候,最简单的,通过 Object.x = y 即可。实际上我们会触发对象上的 [[Put]] 方法,先去查找目标属性,再去设置属性的值
[[Put]] 的原理较为复杂,当设置的属性已经存在时,会进行以下判断
- 是否有我们自定义的 Getter、Setter,如果有就调用我们自己编写的 Setter
- 属性描述符的 writable 是否为 false?如果是,则静默修改失败,在严格模式下会抛出错误
- 以上条件都不是,直接设置
当属性不存在时,就会涉及到原型链上的判断,与 [[Get]] 类型,会沿着原型链查找是否有对应的属性,如果有则修改,如果没有就直接设置。
Getter 与 Setter 是属性上的一个配置方法,可以用来覆盖默认的设置与获取逻辑。
当我们给某一个属性设置了 Getter / Setter 时,该属性就会被成为 访问描述符。对于访问描述符来说,再对属性进行赋值时,JavaScript 引擎会忽略属性设置的 value 和 writable 这两个特性,Getter的返回值会取代value, Setter 的赋值逻辑会取代 writable 配置的逻辑,但 configurable 和 enumerable 这两个配置依然会生效。
const myObject = {
age: 26
}
Object.defineProperty(myObject, 'experience', {
get: function() {
return `${this.age - 23} years`;
},
enumerable: true
})
myObject.a; // 26
myObject.experience; // 3 years
let money;
Object.defineProperty(myObject, 'money', {
get: function() {
return money;
},
set: function(val) {
money = val < 15 ? 15 : val;
}
})
当给一个属性定义 Getter 和 Setter 后,这两者必须是同时出现的,原因如下:
set: function(value) { this.money = value; } 每一次设置 this.money 都会再次触发 set,直至溢出。存在性即判断一个属性是否在一个对象中,常用的两种方法:
in,in 操作符会判断目标属性是否在当前对象及其原型链中,使用方法 property in myObjectmyObject.hasOwnProperty,该方法只会判断目标属性是否在当前对象上,使用方法 myObject.hasOwnPropery(property)for in 可以用来遍历对象的可枚举的属性,数组也是对象的一种,所以 for in 即可以用于普通对象,也可以用于遍历数组
for of 依赖于 ES6 新增的 @@iterator 方法,而普通对象是没有 @@iterator 方法的,所以 for of 只可以用来做数组的遍历