Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
husky-dot opened this issue Oct 14, 2019 · 1 comment
Open

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

husky-dot opened this issue Oct 14, 2019 · 1 comment

Comments

@husky-dot
Copy link
Owner

作者:valentinogagliardi
译者:前端小智
来源:github


阿里云最近在做活动,低至2折,有兴趣可以看看:
https://promotion.aliyun.com/ntms/yunparter/invite.html?userCode=pxuujn3r


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

一切皆对象

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

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

原文:https://github.com/valentinogagliardi/Little-JavaScript-Book/blob/v1.0.0/manuscript/chapter5.md

交流

阿里云最近在做活动,低至2折,有兴趣可以看看:https://promotion.aliyun.com/ntms/yunparter/invite.html?userCode=pxuujn3r

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

https://github.com/qq449245884/xiaozhi

因为篇幅的限制,今天的分享只到这里。如果大家想了解更多的内容的话,可以去扫一扫每篇文章最下面的二维码,然后关注咱们的微信公众号,了解更多的资讯和有价值的内容。

clipboard.png

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

@1442916418
Copy link

1442916418 commented Feb 27, 2020

如何创建不可变的 JS 对象?

Object.preventExtensions()方法让一个对象变的不可扩展,也就是永远不能再添加新的属性。
var superImportantObject = {
  property1: "some string",
  property2: "some other string"
};

Object.preventExtensions(superImportantObject);

superImportantObject.anotherProperty = "Hei!";

console.log(superImportantObject.anotherProperty); // 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”仅适用于对象的第一级:嵌套对象不受操作的影响。

“prototype” 是什么?
prototype 是所有公共方法和属性的宿主,从祖先派生的“子”对象可以从使用祖先的方法和属性。

什么是构造函数调用?
使用new关键字,被称为“构造函数调用”

什么是构造函数?
构造函数 ,是一种特殊的方法。主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中

可以描述一下 new 在底层下做了哪些事吗?

使用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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants