「译」一块儿探讨 JavaScript 的对象


一块儿探讨 JavaScript 的对象

对象是多个属性的动态集合,它有一个连接着原型的隐藏属性(注:__proto__)。javascript

一个属性拥有一个 key 和一个 value 。java

属性的 key

属性的 key 是一个惟一的字符串。git

访问属性有两种方式:点表示法和括号表示法。当使用点表示法,属性的 key 必须是有效的标识符。github

let obj = {
  message : "A message"
}
obj.message //"A message"
obj["message"] //"A message"
复制代码

访问一个不存在的属性不会抛出错误,可是会返回 undefined数组

obj.otherProperty //undefined
复制代码

当使用括号表示法,属性的 key 不要求是有效的标识符 —— 能够是任意值。浏览器

let french = {};
french["thank you very much"] = "merci beaucoup";

french["thank you very much"]; //"merci beaucoup"
复制代码

当属性的key是一个非字符串的值,会用toString()方法(若是可用的话)把它转换为字符串。bash

let obj = {};
//Number
obj[1] = "Number 1";
obj[1] === obj["1"]; //true
//Object
let number1 = {
  toString : function() { return "1"; }
}
obj[number1] === obj["1"]; //true
复制代码

在上面的示例中,对象 number1 被用做一个 key 。它会被转换为字符串,转换结果 “1” 被用做属性的 key 。闭包

属性的值

属性的值能够是任意的基础数据类型,对象,或函数。app

对象做为值

对象能够嵌套在其余对象里。看下面这个例子函数

let book = {
  title : "The Good Parts",
  author : {
    firstName : "Douglas",
    lastName : "Crockford"
  }
}
book.author.firstName; //"Douglas"
复制代码

经过这种方式,咱们就能够建立一个命名空间:

let app = {};
app.authorService = { getAuthors : function() {} };
app.bookService = { getBooks : function() {} };
复制代码

函数做为值

当一个函数被做为属性值,一般成为一个方法。在方法中,this 关键字表明着当前的对象。

this ,会根据函数的调用方式有不一样的值。了解更多关于this 丢失上下文的问题,能够查看当"this"丢失上下文时应该怎么办

动态性

对象本质上就是动态的。能够任意添加删除属性。

let obj = {};
obj.message = "This is a message"; //add new property
obj.otherMessage = "A new message"; //add new property
delete obj.otherMessage; //delete property
复制代码

Map

咱们能够把对象当作一个 Map。Map 的 key 就是对象的属性。

访问一个 key 不须要去扫描全部属性。访问的时间复杂度是 o(1)。

原型

对象有一个连接着原型对象的“隐藏”属性 __proto__,对象是从这个原型对象中继承属性的。

举个例子,使用对象字面量建立的对象有一个指向 Object.prototype 的连接:

var obj = {};
obj.__proto__ === Object.prototype; //true
复制代码

原型链

原型对象有它本身的原型。当一个属性被访问的时候而且不包含在当前对象中,JavaScript会沿着原型链向下查找直到找到被访问的属性,或者到达 null 为止。

只读

原型只用于读取值。对象进行更改时,只会做用到当前对象,不会影响对象的原型;就算原型上有同名的属性,也是如此。

空对象

正如咱们看到的,空对象 {} 并非真正意义上的空,由于它包含着指向 Object.prototype 的连接。为了建立一个真正的空对象,咱们可使用 Object.create(null) 。它会建立一个没有任何属性的对象。这一般用来建立一个Map。

原始值和包装对象

在容许访问属性这一点上,JavaScript 把原始值描述为对象。固然了,原始值并非对象。

(1.23).toFixed(1); //"1.2"
"text".toUpperCase(); //"TEXT"
true.toString(); //"true"
复制代码

为了容许访问原始值的属性, JavaScript 创造了一个包装对象,而后销毁它。JavaScript引擎对建立包装和销毁包装对象的过程作了优化。

数值、字符串和布尔值都有等效的包装对象。跟别是:NumberStringBoolean

nullundefined 原始值没有相应的包装对象而且不提供任何方法。

内置原型

Numbers 继承自Number.prototypeNumber.prototype继承自Object.prototype

var no = 1;
no.__proto__ === Number.prototype; //true
no.__proto__.__proto__ === Object.prototype; //true
复制代码

Strings 继承自 String.prototype。Booleans 继承自 Boolean.prototype

函数都是对象,继承自 Function.prototype 。函数拥有 bind()apply()call() 等方法。

全部对象、函数和原始值(除了 nullundefined )都从 Object.prototype 继承属性。他们都有 toString() 方法。

使用 polyfill 扩充内置对象

JavaScript 能够轻松地使用新功能扩充内置对象。

polyfill 就是一个代码片断,用于在不支持某功能的浏览器中实现该功能。

实用工具

举个例子,这个为 Object.assign() 写的polyfill,若是它不可用,那么就在 Object 上添加一个新方法。

Array.from() 写了相似的polyfill,若是它不可用,就在 Array 上添加一个新方法。

原型

新的方法能够被添加到原型。

举个例子,String.prototype.trim() polyfill让全部的字符串都能使用 trim() 方法。

let text = " A text ";
text.trim(); //"A text"
复制代码

Array.prototype.find() polyfill让全部的数组都能使用find()方法。polyfill也是一样的。

let arr = ["A", "B", "C", "D", "E"];
arr.indexOf("C"); //2
复制代码

单一继承

Object.create() 用特定的原型对象建立一个新对象。它用来作单一继承。思考下面的例子:

let bookPrototype = {
  getFullTitle : function(){
    return this.title + " by " + this.author;
  }
}
let book = Object.create(bookPrototype);
book.title = "JavaScript: The Good Parts";
book.author = "Douglas Crockford";
book.getFullTitle();//JavaScript: The Good Parts by Douglas Crockford
复制代码

多重继承

Object.assign() 从一个或多个对象拷贝属性到目标对象。它用来作多重继承。看下面的例子:

let authorDataService = { getAuthors : function() {} };
let bookDataService = { getBooks : function() {} };
let userDataService = { getUsers : function() {} };
let dataService = Object.assign({},
 authorDataService,
 bookDataService,
 userDataService
);
dataService.getAuthors();
dataService.getBooks();
dataService.getUsers();
复制代码

不可变对象

Object.freeze() 冻结一个对象。属性不能被添加、删除、更改。对象会变成不可变的。

"use strict";
let book = Object.freeze({
  title : "Functional-Light JavaScript",
  author : "Kyle Simpson"
});
book.title = "Other title";//Cannot assign to read only property 'title'
复制代码

Object.freeze() 实行浅冻结。要深冻结,须要递归冻结对象的每个属性。

拷贝

Object.assign() 被用做拷贝对象。

let book = Object.freeze({
  title : "JavaScript Allongé",
  author : "Reginald Braithwaite"
});
let clone = Object.assign({}, book);
复制代码

Object.assign() 执行浅拷贝,不是深拷贝。它拷贝对象的第一层属性。嵌套的对象会在原始对象和副本对象之间共享。

对象字面量

对象字面量提供一种简单、优雅的方式建立对象。

let timer = {
  fn : null,
  start : function(callback) { this.fn = callback; },
  stop : function() {},
}
复制代码

可是,这种语法有一些缺点。全部的属性都是公共的,方法可以被重定义,而且不能在新实例中使用相同的方法。

timer.fn;//null 
timer.start = function() { console.log("New implementation"); }
复制代码

Object.create()

Object.create()Object.freeze() 一块儿可以解决最后两个问题。

首先,我要使用全部方法建立一个冻结原型 timerPrototype ,而后建立对象去继承它。

let timerPrototype = Object.freeze({
  start : function() {},
  stop : function() {}
});
let timer = Object.create(timerPrototype);
timer.__proto__ === timerPrototype; //true
复制代码

当原型被冻结,继承它的对象不可以更改其中的属性。如今,start()stop() 方法不能被从新定义。

"use strict";
timer.start = function() { console.log("New implementation"); } //Cannot assign to read only property 'start' of object
复制代码

Object.create(timerPrototype) 能够用来使用相同的原型构建更多对象。

构造函数

最初,JavaScript 语言提出构造函数做为这些的语法糖。看下面的代码:

function Timer(callback){
  this.fn = callback;
}
Timer.prototype = {
  start : function() {},
  stop : function() {}
}
function getTodos() {}
let timer = new Timer(getTodos);
复制代码

全部的以 function 关键字定义的函数均可以做为构造函数。构造函数使用功能 new 调用。新对象将原型设定为 FunctionConstructor.prototype

let timer = new Timer();
timer.__proto__ === Timer.prototype;
复制代码

一样地,咱们须要冻结原型来防止方法被重定义。

Timer.prototype = Object.freeze({
  start : function() {},
  stop : function() {}
});
复制代码

new 操做符

当执行 new Timer() 时,它与函数 newTimer() 做用相同:

function newTimer(){
  let newObj = Object.create(Timer.prototype);
  let returnObj = Timer.call(newObj, arguments);
  if(returnObj) return returnObj;
    
  return newObj;
}
复制代码

使用 Timer.prototype 做为原型,创造了一个新对象。而后执行 Timer 函数并为新对象设置属性字段。

ES2015为这一切带来了更好的语法糖。看下面的例子:

class Timer{
  constructor(callback){
    this.fn = callback;
  }
  
  start() {}
  stop() {}  
}
Object.freeze(Timer.prototype);
复制代码

使用 class 构建的对象将原型设置为 ClassName.prototype 。在使用类建立对象时,必须使用 new 操做符。

let timer= new Timer();
timer.__proto__ === Timer.prototype;
复制代码

class 语法不会冻结原型,因此咱们须要在以后进行操做。

Object.freeze(Timer.prototype);
复制代码

基于原型的继承

在 JavaScript 中,对象继承自对象。

构造函数和类都是用来建立原型对象的全部方法的语法糖。而后它建立一个继承自原型对象的新对象,并为新对象设置数据字段 基于原型的继承具备保护记忆的好处。原型只建立一次而且由全部的实例使用。

没有封装

基于原型的继承模式没有私有性。全部对象的属性都是公有的。

Object.keys() 返回一个包含全部属性键的数组。它能够用来迭代对象的全部属性。

function logProperty(name){
  console.log(name); //property name
  console.log(obj[name]); //property value
}
Object.keys(obj).forEach(logProperty);
复制代码

模拟的私有模式包含使用 _ 来标记私有属性,这样其余人会避免使用他们:

class Timer{
  constructor(callback){
    this._fn = callback;
    this._timerId = 0;
  }
}
复制代码

工厂模式

JavaScript 提供一种使用工厂模式建立封装对象的新方式。

function TodoStore(callback){
    let fn = callback;
    
    function start() {},
    function stop() {}
    
    return Object.freeze({
       start,
       stop
    });
}
复制代码

fn 变量是私有的。只有 start()stop() 方法是公有的。start()stop() 方法不能被外界改变。这里没有使用 this ,因此没有 this 丢失上下文的问题。

对象字面量依然用于返回对象,可是此次它只包含函数。更重要的是,这些函数是共享相同私有状态的闭包。 Object.freeze() 被用来冻结公有 API。

Timer 对象的完整实现,请看具备封装功能的实用JavaScript对象.

结论

JavaScript 像对象同样处理原始值、对象和函数。

对象本质上是动态的,能够用做 Map。

对象继承自其余对象。构造函数和类是建立从其余原型对象继承的对象的语法糖。

Object.create() 能够用来单一继承,Object.assign() 用来多重继承。

工厂函数能够构建封装对象。

有关 JavaScript 功能的更多信息,请看:

Discover the power of first class functions

How point-free composition will make you a better functional programmer

Here are a few function decorators you can write from scratch

Why you should give the Closure function another chance

Make your code easier to read with Functional Programming

相关文章
相关标签/搜索