【JS 口袋书】第 5 章:JS 对象生命周期的秘密

做者:valentinogagliardihtml

译者:前端小智前端

来源:githubgit


阿里云最近在作活动,低至2折,有兴趣能够看看: promotion.aliyun.com/ntms/yunpar…github


为了保证的可读性,本文采用意译而非直译。编程

一切皆对象

我们常常听到JS中“一切皆对象”? 有没有问想过这是什么意思? 其它语言也有“一切皆对象”之说,如Python。 可是Python中的对象不只仅是像JS对象这样的存放值和值的容器。 Python中的对象是一个。 JS中有相似的东西,但JS中的“对象”只是键和值的容器:json

var obj = { name: "Tom", age: 34 }
复制代码

实际上,JS中的对象是一种“哑”类型,但不少其余实体彷佛都是从对象派生出来的。 甚至是数组,在JS中建立一个数组,以下所示:数组

var arr = [1,2,3,4,5]
复制代码

而后用typeof运算符检查类型,会看到一个使人惊讶的结果:浏览器

typeof arr
"object"
复制代码

看来数组是一种特殊的对象! 即便JS中的函数也是对象。 若是你深刻挖掘,还有更多,建立一个函数,该函数就会附加一些方法:微信

var a = function(){ return false; }
a.toString()
复制代码

输出:编程语言

"function(){ return false; }"
复制代码

我们并无在函数声明toString方法,因此在底层必定还有东西。它从何而来? Object有一个名为.toString的方法。 彷佛我们的函数具备相同的Object方法。

Object.toString()
复制代码

这时我们使用浏览器控制台来查看默认被附加的函数和属性,这个谜团就会变得更加复杂:

谁把这些方法放在函数呢。 JS中的函数是一种特殊的对象,这会不会是个暗示? 再看看上面的图片:咱们的函数中有一个名为prototype的奇怪命名属性,这又是什么鬼?

JS中的prototype是一个对象。 它就像一个背包,附着在大多数JS内置对象上。 例如 Object, Function, Array, Date, Error,都有一个“prototype”:

typeof Object.prototype // 'object'
typeof Date.prototype // 'object'
typeof String.prototype // 'object'
typeof Number.prototype // 'object'
typeof Array.prototype // 'object'
typeof Error.prototype // 'object'
复制代码

注意内置对象有大写字母:

  • String
  • Number
  • Boolean
  • Object
  • Symbol
  • Null
  • Undefined

如下除了Object是类型以外,其它是JS的基本类型。另外一方面,内置对象就像JS类型的镜像,也用做函数。例如,可使用String做为函数将数字转换为字符串:

String(34)
复制代码

如今回到“prototype”。prototype是全部公共方法和属性的宿主,从祖先派生的“子”对象能够从使用祖先的方法和属性。也就是说,给定一个原始 prototype,我们能够建立新的对象,这些对象将使用一个原型做为公共函数的真实源,不 Look see see。

假设有个要求建立一个聊天应用程序,有我的物对象。这我的物能够发送消息,登陆时,会收到一个问候。

根据需求我们很容易定义这个么一 Person 对象:

var Person = {
  name: "noname",
  age: 0,
  greet: function() {
    console.log(`Hello ${this.name}`);
  }
};
复制代码

你可能会想知道,为何这里要使用字面量的方式来声明 Person 对象。 稍后会详细说明,如今该 Person“模型”。经过这个模型,我们使用 Object.create() 来建立觉得这个模型为基础的对象。

建立和连接对象

JS中对象彷佛以某种方式连接在一块儿,Object.create()说明了这一点,此方法从原始对象开始建立新对象,再来建立一个新Person 对象:

var Person = {
  name: "noname",
  age: 0,
  greet: function() {
    console.log(`Hello ${this.name}`);
  }
};

var Tom = Object.create(Person);
复制代码

如今,Tom 是一个新的对象,可是我们没有指定任何新的方法或属性,但它仍然能够访问Person中的nameage 属性。

var Person = {
  name: "noname",
  age: 0,
  greet: function() {
    console.log(`Hello ${this.name}`);
  }
};

var Tom = Object.create(Person);

var tomAge = Tom.age;
var tomName = Tom.name;

console.log(`${tomAge} ${tomName}`);

// Output: 0 noname
复制代码

如今,能够从一个共同的祖先开始建立新的person。但奇怪的是,新对象仍然与原始对象保持链接,这不是一个大问题,由于“子”对象能够自定义属性和方法

var Person = {
  name: "noname",
  age: 0,
  greet: function() {
    console.log(`Hello ${this.name}`);
  }
};

var Tom = Object.create(Person);

Tom.age = 34;
Tom.name = "Tom";
var tomAge = Tom.age;
var tomName = Tom.name;

console.log(`${tomAge} ${tomName}`);

// Output: 34 Tom
复制代码

这种方式被称为“屏蔽”原始属性。 还有另外一种将属性传递给新对象的方法。 Object.create将另外一个对象做为第二个参数,能够在其中为新对象指定键和值:

var Tom = Object.create(Person, {
  age: {
    value: 34
  },
  name: {
    value: "Tom"
  }
});
复制代码

以这种方式配置的属性默认状况下不可写,不可枚举,不可配置。 不可写意味着以后没法更改该属性,更改会被忽略:

var Tom = Object.create(Person, {
  age: {
    value: 34
  },
  name: {
    value: "Tom"
  }
});

Tom.age = 80;
Tom.name = "evilchange";

var tomAge = Tom.age;
var tomName = Tom.name;

Tom.greet();

console.log(`${tomAge} ${tomName}`);

// Hello Tom
// 34 Tom
复制代码

不可枚举意味着属性不会在 for...in 循环中显示,例如:

for (const key in Tom) {
  console.log(key);
}

// Output: greet
复制代码

可是正如我们所看到的,因为JS引擎沿着原型链向上查找,在“父”对象上找到greet属性。最后,不可配置意味着属性既不能修改也不能删除。

Tom.age = 80;
Tom.name = "evilchange";
delete Tom.name;
var tomAge = Tom.age;
var tomName = Tom.name;

console.log(`${tomAge} ${tomName}`);

// 34 Tom
复制代码

若是要更改属性的行为,只需配writable(可写性),configurable(可配置),enumerable(可枚举)属性便可。

var Tom = Object.create(Person, {
  age: {
    value: 34,
    enumerable: true,
    writable: true,
    configurable: true
  },
  name: {
    value: "Tom",
    enumerable: true,
    writable: true,
    configurable: true
  }
});
复制代码

如今,Tom也能够经过如下方式访问greet()

var Person = {
  name: "noname",
  age: 0,
  greet: function() {
    console.log(`Hello ${this.name}`);
  }
};

var Tom = Object.create(Person);

Tom.age = 34;
Tom.name = "Tom";
var tomAge = Tom.age;
var tomName = Tom.name;
Tom.greet();

console.log(`${tomAge} ${tomName}`);

// Hello Tom
// 34 Tom
复制代码

暂时不要过于担忧“this”。 拉下来会详细介绍。暂且先记住,“this”是对函数执行的某个对象的引用。在我们的例子中,greet()Tom的上下文中运行,所以能够访问“this.name”。

构建JavaScript对象

目前为止,只介绍了关于“prototype”的一点知识 ,还有玩了一会 Object.create()以外但我们没有直接使用它。 随着时间的推移出现了一个新的模式:构造函数。 使用函数建立新对象听起来很合理, 假设你想将Person对象转换为函数,你能够用如下方式:

function Person(name, age) {
  var newPerson = {};
  newPerson.age = age;
  newPerson.name = name;
  newPerson.greet = function() {
    console.log("Hello " + newPerson.name);
  };
  return newPerson;
}
复制代码

所以,不须要处处调用object.create(),只需将Person做为函数调用:

var me = Person("Valentino");
复制代码

构造函数模式有助于封装一系列JS对象的建立和配置。 在这里, 我们使用字面量的方式建立对象。 这是一种从面向对象语言借用的约定,其中类名开头要大写。

上面的例子有一个严重的问题:每次我们建立一个新对象时,一遍又一遍地重复建立greet()函数。可使用Object.create(),它会在对象之间建立连接,建立次数只有一次。 首先,我们将greet()方法移到外面的一个对象上。 而后,可使用Object.create()将新对象连接到该公共对象:

var personMethods = {
  greet: function() {
    console.log("Hello " + this.name);
  }
};

function Person(name, age) {
  // greet lives outside now
  var newPerson = Object.create(personMethods);
  newPerson.age = age;
  newPerson.name = name;
  return newPerson;
}

var me = Person("Valentino");
me.greet();

// Output: "Hello Valentino"
复制代码

这种方式比刚开始会点,还能够进一步优化就是使用prototypeprototype是一个对象,能够在上面扩展属性,方法等等。

Person.prototype.greet = function() {
  console.log("Hello " + this.name);
};
复制代码

移除了personMethods。 调整Object.create的参数,不然新对象不会自动连接到共同的祖先:

function Person(name, age) {
  // greet lives outside now
  var newPerson = Object.create(Person.prototype);
  newPerson.age = age;
  newPerson.name = name;
  return newPerson;
}

Person.prototype.greet = function() {
  console.log("Hello " + this.name);
};

var me = Person("Valentino");
me.greet();

// Output: "Hello Valentino"
复制代码

如今公共方法的来源是Person.prototype。 使用JS中的new运算符,能够消除Person中的全部噪声,而且只须要为this分配参数。

下面代码:

function Person(name, age) {
  // greet lives outside now
  var newPerson = Object.create(Person.prototype);
  newPerson.age = age;
  newPerson.name = name;
  return newPerson;
}
复制代码

改为:

function Person(name, age) {
  this.name = name;
  this.age = age;
}
复制代码

完整代码:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.greet = function() {
  console.log("Hello " + this.name);
};

var me = new Person("Valentino");
me.greet();

// Output: "Hello Valentino"
复制代码

注意,使用new关键字,被称为“构造函数调用”new 干了三件事情

  • 建立一个空对象

  • 将空对象的__proto__指向构造函数的prototype

  • 使用空对象做为上下文的调用构造函数

    function Person(name, age) { this.name = name; this.age = age; }

根据上面描述的,new Person("Valentino") 作了:

  • 建立一个空对象:var obj = {}
  • 将空对象的__proto__指向构造函数的 prototype:obj.__proto__ = Person().prototype
  • 使用空对象做为上下文调用构造函数: Person.call(obj)

检查原型链

检查JS对象之间的原型连接有不少种方法。 例如,Object.getPrototypeOf是一个返回任何给定对象原型的方法。 考虑如下代码:

var Person = {
  name: "noname",
  age: 0,
  greet: function() {
    console.log(`Hello ${this.name}`);
  }
};

var Tom = Object.create(Person);
复制代码

检查Person是不是Tom的原型:

var tomPrototype = Object.getPrototypeOf(Tom);

console.log(tomPrototype === Person);

// Output: true
复制代码

固然,若是使用构造函数调用构造对象,Object.getPrototypeOf也能够工做。 可是应该检查原型对象,而不是构造函数自己:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.greet = function() {
  console.log("Hello " + this.name);
};

var me = new Person("Valentino");

var mePrototype = Object.getPrototypeOf(me);

console.log(mePrototype === Person.prototype);

// Output: true
复制代码

除了Object.getPrototypeOf以外,还有另外一个方法isPrototypeOf。 该方法用于测试一个对象是否存在于另外一个对象的原型链上,以下所示,检查 me 是否在 Person.prototype 上:

Person.prototype.isPrototypeOf(me) && console.log('Yes I am!')
复制代码

instanceof运算符也能够用于测试构造函数的prototype属性是否出如今对象的原型链中的任何位置。 老实说,这个名字有点误导,由于JS中没有“实例”。 在真正的面向对象语言中,实例是从类建立的新对象。 请考虑Python中的示例。 我们有一个名为Person的类,我们从该类建立一个名为“tom”的新实例:

class Person():
    def __init__(self, age, name):
        self.age = age;
        self.name = name;

    def __str__(self):
        return f'{self.name}'
        

tom = Person(34, 'Tom')
复制代码

注意,在Python中没有new关键字。如今,我们可使用isinstance方法检查tom是不是Person的实例

isinstance(tom, Person)

// Output: True
复制代码

Tom也是Python中“object”的一个实例,下面的代码也返回true

isinstance(tom, object)

// Output: True
复制代码

根据isinstance文档,“若是对象参数是类参数的实例,或者是它的(直接、间接或虚拟)子类的实例,则返回true”。我们在这里讨论的是类。如今让我们看看instanceof作了什么。我们将从JS中的Person函数开始建立tom(由于没有真正的类)

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.greet = function() {
  console.log(`Hello ${this.name}`);
};

var tom = new Person(34, "Tom");
复制代码

使用isinstance方法检查tom是不是PersonObject 的实例

if (tom instanceof Object) {
  console.log("Yes I am!");
}

if (tom instanceof Person) {
  console.log("Yes I am!");
}
复制代码

所以,能够得出结论:JS对象的原型老是链接到直接的“父对象”和Object.prototype。没有像PythonJava这样的类。JS是由对象组成,那么什么是原型链呢?若是你注意的话,我们提到过几回“原型链”。JS对象能够访问代码中其余地方定义的方法,这看起来很神奇。再次考虑下面的例子:

var Person = {
  name: "noname",
  age: 0,
  greet: function() {
    console.log(`Hello ${this.name}`);
  }
};

var Tom = Object.create(Person);

Tom.greet();
复制代码

即便该方法不直接存在于“Tom”对象上,Tom也能够访问greet()

这是JS的一个内在特征,它从另外一种称为Self的语言中借用了原型系统。 当访问greet()时,JS引擎会检查该方法是否可直接在Tom上使用。 若是不是,搜索将继续向上连接,直到找到该方法。

“链”是Tom链接的原型对象的层次结构。 在咱们的例子中,TomPerson类型的对象,所以Tom的原型链接到Person.prototype。 而Person.prototypeObject类型的对象,所以共享相同的Object.prototype原型。 若是在Person.prototype上没有greet(),则搜索将继续向上连接,直到到达Object.prototype。 这就是我们所说的**“原型链”**。

保护对象不受操纵

大多数状况下,JS 对象“可扩展”是必要的,这样我们能够向对象添加新属性。 但有些状况下,咱们但愿对象不受进一步操纵。 考虑一个简单的对象:

var superImportantObject = {
  property1: "some string",
  property2: "some other string"
};
复制代码

默认状况下,每一个人均可以向该对象添加新属性

var superImportantObject = {
  property1: "some string",
  property2: "some other string"
};

superImportantObject.anotherProperty = "Hei!";

console.log(superImportantObject.anotherProperty); // Hei!
复制代码

Object.preventExtensions()方法让一个对象变的不可扩展,也就是永远不能再添加新的属性。

var superImportantObject = {
  property1: "some string",
  property2: "some other string"
};

Object.preventExtensions(superImportantObject);

superImportantObject.anotherProperty = "Hei!";

console.log(superImportantObject.anotherProperty); // undefined
复制代码

这种技术对于“保护”代码中的关键对象很是方便。JS 中还有许多预先建立的对象,它们都是为扩展而关闭的,从而阻止开发人员在这些对象上添加新属性。这就是“重要”对象的状况,好比XMLHttpRequest的响应。浏览器供应商禁止在响应对象上添加新属性

var request = new XMLHttpRequest();
request.open("GET", "https://jsonplaceholder.typicode.com/posts");
request.send();
request.onload = function() {
  this.response.arbitraryProp = "我是新添加的属性";
  console.log(this.response.arbitraryProp); // undefined
};
复制代码

这是经过在“response”对象上内部调用Object.preventExtensions来完成的。 您还可使用Object.isExtensible方法检查对象是否受到保护。 若是对象是可扩展的,它将返回true

var superImportantObject = {
  property1: "some string",
  property2: "some other string"
};

Object.isExtensible(superImportantObject) && console.log("我是可扩展的");
复制代码

若是对象不可扩展的,它将返回false

var superImportantObject = {
  property1: "some string",
  property2: "some other string"
};

Object.preventExtensions(superImportantObject);

Object.isExtensible(superImportantObject) ||
  console.log("我是不可扩展的!");
复制代码

固然,对象的现有属性能够更改甚至删除

var superImportantObject = {
  property1: "some string",
  property2: "some other string"
};

Object.preventExtensions(superImportantObject);

delete superImportantObject.property1;

superImportantObject.property2 = "yeees";

console.log(superImportantObject); // { property2: 'yeees' }
复制代码

如今,为了防止这种操做,能够将每一个属性定义为不可写和不可配置。为此,有一个方法叫Object.defineProperties

var superImportantObject = {};

Object.defineProperties(superImportantObject, {
  property1: {
    configurable: false,
    writable: false,
    enumerable: true,
    value: "some string"
  },
  property2: {
    configurable: false,
    writable: false,
    enumerable: true,
    value: "some other string"
  }
});
复制代码

或者,更方便的是,能够在原始对象上使用Object.freeze

var superImportantObject = {
  property1: "some string",
  property2: "some other string"
};

Object.freeze(superImportantObject);
复制代码

Object.freeze工做方式与Object.preventExtensions相同,而且它使全部对象的属性不可写且不可配置。 惟一的缺点是“Object.freeze”仅适用于对象的第一级:嵌套对象不受操做的影响。

class

有大量关于ES6 类的文章,因此在这里只讨论几点。JS是一种真正的面向对象语言吗?看起来是这样的,若是我们看看这段代码

class Person {
  constructor(name) {
    this.name = name;
  }

  greet() {
    console.log(`Hello ${this.name}`);
  }
}
复制代码

语法与Python等其余编程语言中的类很是类似:

class Person:
    def __init__(self, name):
        self.name = name
    
    def greet(self):
        return 'Hello' + self.name
复制代码

或 PHP

class Person {
    public $name; 

    public function __construct($name){
        $this->name = $name;
    }

    public function greet(){
        echo 'Hello ' . $this->name;
    }
}
复制代码

ES6中引入了类。可是在这一点上,我们应该清楚JS中没有“真正的”类。 一切都只是一个对象,尽管有关键字class,“原型系统”仍然存在。 新的JS版本是向后兼容的,这意味着在现有功能的基础上添加了新功能,这些新功能中的大多数都是遗留代码的语法糖。

总结

JS中的几乎全部东西都是一个对象。 从字面上看。 JS对象是键和值的容器,也可能包含函数。 Object是JS中的基本构建块:所以能够从共同的祖先开始建立其余自定义对象。 而后我们能够经过语言的内在特征将对象连接在一块儿:原型系统。

从公共对象开始,能够建立共享原始“父”的相同属性和方法的其余对象。 可是它的工做方式不是经过将方法和属性复制到每一个孩子,就像OOP语言那样。 在JS中,每一个派生对象都保持与父对象的链接。 使用Object.create或使用所谓的构造函数建立新的自定义对象。 与new关键字配对,构造函数相似于模仿传统的OOP类。

思考

  • 如何建立不可变的 JS 对象?
  • 什么是构造函数调用?
  • 什么是构造函数?
  • “prototype” 是什么?
  • 能够描述一下 new 在底层下作了哪些事吗?

代码部署后可能存在的BUG无法实时知道,过后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给你们推荐一个好用的BUG监控工具 Fundebug

原文:github.com/valentinoga…

交流(欢迎加入群,群工做日都会发红包,互动讨论技术)

阿里云最近在作活动,低至2折,有兴趣能够看看:promotion.aliyun.com/ntms/yunpar…

干货系列文章汇总以下,以为不错点个Star,欢迎 加群 互相学习。

github.com/qq449245884…

由于篇幅的限制,今天的分享只到这里。若是你们想了解更多的内容的话,能够去扫一扫每篇文章最下面的二维码,而后关注我们的微信公众号,了解更多的资讯和有价值的内容。

clipboard.png

每次整理文章,通常都到2点才睡觉,一周4次左右,挺苦的,还望支持,给点鼓励

相关文章
相关标签/搜索