本文转载自微信公众号「勾勾的前端世界」,作者西岭。转载本文请联系勾勾的前端世界公众号。
回忆一下什么是对象:Coding 第一奥义:面向对象编程
JavaScript 语言本身的设计缺陷,误打误撞,成了解释最为彻底的“世界原本的样子”的计算机编程语言;
——西岭《凡人凡语》
Everything is object (万物皆对象),JS 语言中将一切都视为 对象 。
JavaScript 语言的对象体系,不基于“类” 创建对象,是基于构造函数(constructor)和原型链(prototype)。
简单方式创建对象
我们可以直接通过 new Object() 创建:
- var person = new Object()
- person.name = 'Jack'
- person.age = 18
-
- person.sayName = function () {
- console.log(this.name)
- }
字面量方式创建对象
每次创建通过 new Object() 比较麻烦,所以可以通过它的简写形式对象字面量来创建:
- var person = {
- name: 'Jack',
- age: 18,
- sayName: function () {
- console.log(this.name)
- }
- }
构造函数
JavaScript 语言使用构造函数作为对象的模板。
所谓 "构造函数",就是一个普通的函数,只不过我们专门用它来生成对象,这样使用的函数,就是构造函数。
它提供模板,描述对象的基本结构。一个构造函数,可以生成多个对象,这些对象都有相同的结构。
- function Person (name, age) {
- this.name = name
- this.age = age
- this.sayName = function () {
- console.log(this.name)
- }
- }
-
- var p1 = new Person('Jack', 18)
- p1.sayName() // => Jack
-
- var p2 = new Person('Mike', 23)
- p2.sayName() // => Mike
解析构造函数代码的执行
在上面的示例中,使用 new 操作符创建 Person 实例对象;
以这种方式调用构造函数会经历以下 5 个步骤:
- 创建一个空对象,作为将要返回的对象实例。
- 将这个空对象的原型,指向构造函数的prototype属性。先记住,后面讲
- 将这个空对象赋值给函数内部的this关键字。
- 执行构造函数内部的代码。
- 返回新对象 (this)
- function Person (name, age) {
- // 当使用 new 操作符调用 Person() 的时候,实际上这里会先创建一个对象
- // 然后让内部的 this 指向新创建的对象
- // 接下来所有针对 this 的操作实际上操作的就是刚创建的这个对象
-
- this.name = name
- this.age = age
- this.sayName = function () {
- console.log(this.name)
- }
-
- // 在函数的结尾处会将 this 返回,也就是这个新对象
- }
构造函数和实例对象的关系
构造函数是根据具体的事物抽象出来的抽象模板,实例对象是根据抽象的构造函数模板得到的具体实例对象。
实例对象由构造函数而来,一个构造函数可以生成很多具体的实例对象,而每个实例对象都是独一无二的。
每个对象都有一个 constructor 属性,该属性指向创建该实例的构造函数。
反推出来,每一个对象都有其构造函数
- console.log(p1.constructor === Person) // => true
- console.log(p2.constructor === Person) // => true
- console.log(p1.constructor === p2.constructor) // => true
因此,我们可以通过实例对象的 constructor 属性判断实例和构造函数之间的关系。
构造函数存在的问题
以构造函数为模板,创建对象,对象的属性和方法都可以在构造函数内部定义。
- function Cat(name, color) {
- this.name = name;
- this.color = color;
- this.say = function () {
- console.log('hello'+this.name,this.color);
- };
- }
- var cat1 = new Cat('猫', '白色');
- var cat2 = new Cat('猫', '黑色');
- cat1.say();
- cat2.say();
在该示例中,从表面上看好像没什么问题,但是实际上这样做,有一个很大的弊端。那就是对于每一个实例对象, name 和 say 都是一模一样的内容,每一次生成一个实例,都必须为重复的内容,多占用一些内存,如果实例对象很多,会造成极大的内存浪费。
那么,能不能将相同的内容,放到公共部分,节约计算机资源呢?
原型
JavaScript 的每个对象都会继承一个父级对象,父级对象称为 原型 (prototype) 对象。
原型也是一个对象,原型对象上的所有属性和方法,都能被子对象 (派生对象) 共享,通过构造函数生成实例对象时,会自动为实例对象分配原型对象。而每一个构造函数都有一个prototype属性,这个属性就是实例对象的原型对象。
null 没有自己的原型对象。
这也就意味着,我们可以把所有对象实例需要共享的属性和方法直接定义在构造函数的 prototype 属性上,也就是实例对象的原型对象上。
- function Cat(color) {
- this.color = color;
- }
-
- Cat.prototype.name = "猫";
- Cat.prototype.sayhello = function(){
- console.log('hello'+this.name,this.color);
- }
- Cat.prototype.saycolor = function (){
- console.log('hello'+this.color);
- }
-
- var cat1 = new Cat('白色');
- var cat2 = new Cat('黑色');
- cat1.sayhello();
- cat2.saycolor();
这时所有实例对象的 name 属性和 sayhello() 、saycolor 方法,其实都是在同一个内存地址的对象中,也就是构造函数的 prototype 属性上,因此就提高了运行效率节省了内存空间。
原型及原型链
构造函数的 prototyp 属性,就是由这个构造函数 new 出来的所有实例对象的 原型对象
所有对象都有原型对象。
- function Cat(name, color) {
- this.name = name;
- }
-
- var cat1 = new Cat('猫');
-
- console.log(cat1.__proto__.__proto__.__proto__);
而原型对象中的属性和方法,都可以被实例对象直接使用。
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。
- 搜索首先从对象实例本身开始
- 如果在实例中找到了具有给定名字的属性,则返回该属性的值
- 如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性
- 如果在原型对象中找到了这个属性,则返回该属性的值
- 如果还是找不到,就到原型的原型去找,依次类推。
- 如果直到最顶层的Object.prototype还是找不到,则返回undefined。
而这正是多个对象实例共享原型所保存的属性和方法的基本原理。
对象的属性和方法,有可能是定义在自身内,也有可能是定义在它的原型对象上。由于原型本身也是对象,又有自己的原型,所以形成了一条可向上追溯的链条,叫 原型链(prototype chain)。
注意,不在要原型上形成多层链式查找,非常浪费资源。
内置标准库与包装对象
在内置标准对象中,对象是 JavaScript 语言最主要的数据类型,三种原始类型的值——数值、字符串、布尔值——在一定条件下,也会自动转为对象,也就是原始类型的“包装对象”(wrapper)。
所谓“包装对象”,就是分别与数值、字符串、布尔值相对应的Number、String、Boolean三个原生对象。这三个原生对象可以把原始类型的值变成(包装成)对象。
- var v1 = new Number(123);
- var v2 = new String('abc');
- var v3 = new Boolean(true);
-
- typeof v1 // "object"
- typeof v2 // "object"
- typeof v3 // "object"
-
- v1 === 123 // false
- v2 === 'abc' // false
- v3 === true // false
包装对象的最大目的,首先是使得 JavaScript 的对象涵盖所有的值,其次使得原始类型的值可以方便地调用某些方法。
原始类型的值,可以自动当作对象调用,即调用各种对象的方法和参数。
这时,JavaScript 引擎会自动将原始类型的值转为包装对象实例,在使用后立刻销毁实例。
比如,字符串可以调用length属性,返回字符串的长度。
- 'abc'.length // 3
上面代码中,abc是一个字符串,本身不是对象,不能调用length属性。JavaScript 引擎自动将其转为包装对象,在这个对象上调用length属性。调用结束后,这个临时对象就会被销毁。这就叫原始类型与实例对象的自动转换。