Talk about JavaScript object - personal summary

Foreword

Doubt, suspicion and thinking

In the end is still JavaScript object based on object-oriented?

Compared with other languages, JavaScript always seems less gregarious. such as:

  • Unlike other object-oriented languages, JavaScript has no concept of class (before ES6), ES6 arrival also does not change the essence of it is based on a prototype, this is the most confusing place for developers
  • _proto_ and prototype innocently tell
  • Objects can be instantiated by the new keyword can also be directly defined by curly braces
  • JavaScript object attributes can be added freely, but not other languages

Being criticized and debate, someone shouted JavaScript is not "non-Object-oriented" language, but "object-based" language. However, how to define the "object-oriented" and "object-based" is basically hardly anyone can answer.

JavaScript in the end the need for analog class class?

It needs to understand design of the JavaScript language, in order to more clearly in the end the need for simulation class, and why you need analog class. In the early people used to object-oriented programming languages ​​other way, while JavaScript confused and try to close the class with a way to programming.

Origin and Rethinking

What is object-oriented?

Let's look at the definition of JavaScript objects: "Language and host infrastructure is provided by the object, and JavaScript object program means one set of serial communication with each other." The meaning here is not weakened object-oriented expression means, but is subject to the importance of language expression.

What is object-oriented in the end? Objcet in English, is a general term for all things, and abstract thinking that object-oriented programming have in common. Chinese translation of "object" had no such universality. In different programming languages, designers also use a variety of language features to describe the object, the most successful of the genre is to use the "class" way to describe the object, which was born as C ++, Java and other popular programming languages.

The JavaScript Early chose a more popular genre: the prototype. This is one of the reasons unsocial.

Unfortunately, some companies because of political reasons, JavaScript launched by the management of the orders was asked to imitate Java, so, JavaScript founder Brendan Eich on the basis of "prototype running" on the introduction of the new, this and other language features, so that it "looks more like Java".

Therefore, we at least need to understand one thing, before we know the programming "object-oriented" approach, in fact, is "class-based" object-oriented, it is not all object-oriented, rather, just based on the class object-oriented programming a genre only. And want to understand JavaScript object, we must empty our understanding related to the concept of "class-based object-oriented" and return to the basic theory of human naive understanding of the subject and language-independent, we can understand the idea of ​​an object-oriented JavaScript design.

What is a prototype? What is class?

"Class-based" programming promotes the use of a focus on the relationship between the classification and class development model. In such languages, always have the first class, then the class to instantiate an object. Between classes and inheritance may form relationships, and other combinations. Class is often integrated with the language type system, the ability to form a certain compile time.

On the other hand, the "prototype based on the" look more to promote programming programmer to focus on the behavior of a series of object instances, and how to care after these objects, divided into similar recent use prototype object, instead of moving them into classes.

Based on the prototype class and are able to meet the basic needs of reuse and abstraction, but not the same scenario applies. It's like professionals may prefer to see the tiger when favorite is a leopard subspecies of leopard cats used to describe it, but for some less formal occasions, the "big cat" might be more intuitive feel closer to some of. JavaScript is not our first language prototype before it, self, kevo other languages ​​have begun to use the prototype to describe the object.

JavaScript prototype object

JavaScript Prototype:

Despite simulation of complex Java syntax facilities (new, Function Object, prototype properties of the function, etc.), the prototype system can be quite simple and can be summed up with two:

  • If all objects have a private field [[prototype]], it is the prototype of the object;
  • Read a property, if the object itself does not, it will continue to access the prototype object until the prototype is empty or found.

This model in all previous versions and history has not changed, offers some built-in functions listed in ES6, more direct access to the operating prototype:

  • Object.create create a new object with the specified prototype, the prototype can be null;
  • Object.getPrototypeOf prototype of an object is obtained;
  • Object.setPrototypeOf set an object's prototype

Use of these three methods, we can totally put aside thinking class, use the prototype to achieve abstraction and reuse. E.g:

// This code creates a "cat" objects, and in accordance with the cat made some changes to create a tiger, then we can use to create additional Object.create cat and tiger object, we can use the "cat original object" and "tiger original object" to control the behavior of all cats and tigers 
var cAT = { 
    say () { 
        the console.log ( "Meow ~"); 
    }, 
    Jump () { 
        the console.log ( "Jump"); 
    } 
} 

var tiger the Object.create = (CAT, { 
    say: { 
        Writable: to true, 
        Configurable: to true, 
        Enumerable: to true, 
        value: function () { 
            the console.log ( "! Roar"); 
        } 
    } 
}) 


var anotherCat the Object.create = ( CAT); 

anotherCat.say (); 

var = anotherTiger the Object.create (Tiger); 

anotherTiger.say ();

This creates a "cat" objects, and in accordance with the cat made some changes to create a tiger, then we can use to create additional Object.create cat and tiger object, we can use the "cat original object" and "original tiger object "to control the behavior of all cats and tigers. However, in earlier versions, programmers can only be manipulated by Java-style class interface prototype is running, it can be very uncomfortable.

It features a JavaScript object:

  • Object has a unique identity: the logo is not see this variable name, but the memory address
  • Stateful objects
  • Objects have behavior
  • Objects are dynamic

About the unique identification of, if two objects are defined exactly the same structure and values, two of them are not equal (a1 === a2 is false)

About state and behavior, different languages ​​have different description, the Java call them "Properties" and "method", in JavaScript, unified state and behavior abstracted as "property."

The first three features of any object-oriented language with, and JavaScript object is unique characteristics: the object is highly dynamic, given the ability to add objects to change the status and behavior of the user at runtime.

For example, the following example shows the added attribute to an object, this operation is completely OK:

let o = { a: 1 };
o.b = 2;
console.log(o.a, o.b); //1 2

Meanwhile, JavaScript properties are designed to be more complex than other forms of language, which provides data attributes (description attribute) and two types of attributes accessor property descriptor (can be understood as the property of the property, these two types of property descriptor not both, can select only one) .

Data attributes to achieve at a very early version, visit the new properties in ES6. Details can be found summarized below, it is simply: data attributes may specify the value of a property, whether written, and is enumerable, whether can modify the configuration (include whether to delete); accessor property is mainly defined at the time of visit behavior and results.

Since ES6 With class, we can not abandon the prototype?

现在可以回答JavaScript是否需要模拟类这个问题了。其实,JavaScript本不需要模拟类,但是因为大家习惯于类的编程方式(以及一些其他原因,总结在下方),ES6正式开始使用class关键字,new + function的怪异搭配可以完全抛弃了。我们推荐在任何场景都使用ES6的语法来定义类。但在这里需要说明,class关键字的使用,其本质还是基于原型,class extends 只是语法糖,完全不存在抛弃原型一说。

现在,我们可以总结一下为什么JavaScript对象让人困惑了:

1.大部分面向对象语言都是基于类的流派,而基于原型的比较小众。

  基于原型本是一个优秀的抽象对象的形式,但是“基于类”的面向对象已经先入为主成为大部分人的思维,在缺乏系统性学习的前提下,尝试用基于类的思想去理解并掌握基于原型的处理方式,只会让人更加怀疑和困惑。

  其实,不止有人对基于原型有疑问,也有人对基于类表达过疑惑,只是对于大部分人来说质疑一个如此成功的流派显得多余,慢慢地认为面向对象理所应当就是这样。

2.早期由于公司政治原因,模仿java的一些语法和方式,不仅怪异,更加深了人们的困惑。

全面认识JavaScript对象

JavaScript对象分类

JavaScript 对象并非只有一种,比如在浏览器环境中我们无法单纯依靠js代码实现 div 对象,只能靠 document.createElement 来创建,这说明了 JavaScript 的对象机制并非简单的属性集合 + 原型。

JavaScript 中的对象可以分为以下几类,这也与 JavaScript 语言的组成有关:

  • 宿主对象(host Objects):由 JavaScript 宿主环境提供的对象,它们的行为完全由宿主环境决定。
  • 内置对象(Built-in Objects):由 JavaScript 语言提供的对象。
    • 固有对象(Intrinsic Objects):由标准规定,随着 JavaScript 运行时自动创建的对象
    • 原生对象(Native Objects):可以由用户通过内置构造器创建的对象
    • 普通对象(Ordinary Objects):由 {} 语句、Object 构造器或者 class 关键字定义的对象,它能够被原型继承

宿主对象

在浏览器环境中,我们知道全局对象是 window ,window 上又有很多属性,这里的属性一部分来自 JavaScript 语言,一部分来自浏览器环境。JavaScript 标准中规定了全局对象属性,来自浏览器宿主部分的可以理解为 W3C 的 HTML 标准(或非标准)的API,例如DOM、BOM。

固有对象

固有对象是由标准规定,随着 JavaScript 运行时而自动创建的对象实例。这些对象在任何JavaScript代码执行之前就已经被创建出来了,他们通常扮演类似基础库的角色,例如 global 对象(浏览器环境中是 window 对象)、JSON、Math、Number等。

ECMA标准提供了一份并不全面的固有对象表 ECMA

原生对象

能够通过语言本身的构造器创建的对象称作原生对象。在JavaScript标准中,提供了30多个构造器,如下:

几乎所有这些构造器的能力都是无法用纯JavaScript代码实现的,它们也无法用 class/extend 语法来继承。我们可以认为,所有这些原生对象都是为了特定能力或者性能,设计出来的“特权对象”。

ES6以来的扩展(普通对象)

1.简洁的表示方法

let name = "...", age = 20;
let obj = { name: name, age: age, func: function () { ... } }; // ES5表示
let obj = { name, age, func() { ... } }; // ES6表示

2.属性名表达式

使用方括号,属性名称可以用表达式表示,也可以用变量名。

let name = "abc", obj = {};
obj[name] = "123";    // obj :{ abc: "123" }
obj["h" + "ello"] = "aa";  // obj:{ abc: "123", "hello": "aa" }

3.方法的 name 属性

函数有 name 属性,返回函数名。对象内的方法也是函数,也有 name 属性。

const person = {
  sayName() {
    console.log('hello!');
  },
};

person.sayName.name   // "sayName"

4.属性描述符

对象内的每个属性都有一个属性描述符,分为两类:数据属性(也叫描述属性)、访问器属性。两者不能一起用。

数据属性

我们通过 Object.getOwnPropertyDescriptor() 方法来获取某个对象的某个属性的描述符。

let obj = { a: "hello" };
const descriptor = Object.getOwnPropertyDescriptor(obj, 'a');
console.log(descriptor);
{
    value: "hello"
    writable: true
    enumerable: true
    configurable: true
}

可以看到,属性描述符(数据属性)由四个值组成:

  • value  该属性的值
  • writable  属性是否可写,如果为false就成为了只读属性。默认为true
  • enumerable  属性是否可枚举,默认为true
  • configurable  属性是否可配置。为true表示此属性的属性描述符可以被修改,属性也可以被删除,false则不可删除不可修改。默认为true

如何设置属性描述符呢?通过 Object.defineProperty()/Object.defineProperties() 方法:

Object.defineProperty(obj, prop, descriptor) 在对象上定义/修改一个属性,并返回该对象
例:
    let obj = {};
    let des = Object.defineProperty(obj, "a", {value: 123, writable: false});
    obj.a // 123
    console.log(des)  // {value: 123, writable: false,enumerable: true,configurable: true} // enumerable和configurable默认为true

Object.defineProperties(obj, props) 在对象上定义/修改多个属性,并返回该对象
例:
    let obj = {};
    let des = Object.defineProperty(obj, {
        a: {value: 123, writable: true},
        b: {value: 456, writable: true},
    });

访问器属性

访问器属性同样有四个:value、writable、get、set

其中最有用的是 getter/setter 函数,当通过对象取值/赋值时,会触发对应的函数。例如:

let obj = {
    _name: "hello",
    get name() { return this._name },
    set name(value) { this._name = value },
};
obj.name // "hello"
obj.name = "haha";
obj.name // "haha"

需要注意,当定义了 setter/getter 函数后,name属性真实存在,但在取值/赋值函数内部无法获取到同名属性 name ,也就是说,不能将 getter/setter 函数名和属性名相同,这点与 Proxy 不同。在上面的例子中,当访问 name 属性时实际访问的是 _name 属性。

另外,上面提到,不能同时使用两种属性描述符,否则会报错,如:

let obj = {};
Object.defineProperty(obj, "a", {
  get : function(){
    return bValue;
  },
  set : function(newValue){
    bValue = newValue;
  },
  writable: true,  // 该属性只能用于数据描述符
});
// throws a TypeError: value appears only in data descriptors, get appears only in accessor descriptors

5.super关键字

我们知道,this 关键字总是指向函数所在的当前对象,ES6新增了一个类似的关键字 super,指向当前对象的原型对象。

const proto = {
  foo: 'hello'
};

const obj = {
  foo: 'world',
  find() {
    return super.foo;
  }
};

Object.setPrototypeOf(obj, proto);
obj.find() // "hello" obj对象通过super关键字引用了原型对象的foo属性

注意:当super关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。

6.遍历

遍历涉及到属性是否可枚举、是否是自身的属性、键是否是Symbol等问题。

以下四个操作无法遍历对象的不可枚举属性:

for ... in循环:只遍历对象自身的和继承的可枚举属性(不包含Symbol属性)
Object.keys():返回自身可枚举属性的键(不包含Symbol属性)
JSON.stringify():只序列化自身的可枚举属性(不包含Symbol属性)
Object.assign():只拷贝自身的可枚举属性(包含Symbol属性)

另外还有三种操作可以遍历不可枚举属性:

Object.getOwnPropertyNames():返回自身的所有属性(不含Symbol属性)
Object.getOwnPropertySymbols():返回自身所有的Symbol属性
Reflect.ownKeys():返回自身的所有属性(包含Symbol)

以上7种方法,除了JSON.stringify()和assign(),另外五种遍历,遵循同样的次序规则:

  • 首先遍历所有数值键,按照数值升序排列。
  • 其次遍历所有字符串键,按照加入时间升序排列。
  • 最后遍历所有 Symbol 键,按照加入时间升序排列。

7.扩展方法

一览表:

Object.is()        判断两个值是否相同,与===基本一致,可以说是对其的完善
Object.assign()      复制、合并对象,返回新对象。(1.只会复制可枚举属性2.属性都是浅拷贝)
Object.getOwnPropertyDescriptor()    返回某对象的某个自有属性的描述属性
Object.getPrototypeOf()      返回指定对象的原型对象
Object.setPrototypeOf()      设置指定对象的原型对象
Object.keys()          返回一个对象的所有可枚举属性名称
Object.values()         返回一个对象的所有可枚举属性的值
Object.entries()        返回一个对象的所有可枚举属性的键值对数组(可以看做是上面两个方法的结合)
Object.fromEntries()    entries()的逆操作,用于将一个键值对数组还原为对象,因此特别适合将Map结构转为对象

详情见这篇博客 JavaScript字符串、数组、对象方法总结

Class及继承

class关键字的使用正是迎合了“模拟类”的需求,但不改变其基于原型的本质,class及extends只是语法糖。详见 ECMAScript新语法、特性总结

Proxy

Proxy使得我们拥有强大的对象操作能力。Proxy英文意思为“代理”,表示它可以代理某些操作。Proxy 在目标对象前架设一层拦截,外界对该对象的访问,都必须先经过这层拦截,它提供了一种机制,可以对外界的访问进行过滤和改写。这等同于在语言层面做出修改,属于一种“元编程”(meta programmin),即对编程语言进行编程。

const proxy = new Proxy(target, handler);  // 生成 Proxy 实例

栗子(拦截读取操作):

var person = {
  name: "张三"
};
 
var proxy = new Proxy(person, {
  get: function(target, propKey, receiver) {
    if (property in target) {
      return target[property];
    } else {
      throw new ReferenceError("不存在的");
    }
  },
  set: function(target, propKey, value, receiver) {
    console.log("setter", target, propKey, value);
    retrun target[propKey] = value;
  }
});
 
proxy.name // "张三"
proxy.age // 抛出一个错误
proxy.name = "李四"
proxy.name // "李四"

Proxy支持的拦截操作一览表,一共13种:

get(target, propKey, receiver):拦截对象属性的读取
set(target, propKey, value, receiver):拦截对象属性的设置,返回一个布尔值。
has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
construct(target, args):拦截 Proxy 实例作为构造函数调用的操作(new命令),比如new proxy(...args)。

Reflect

Reflect 对象与 Proxy 对象一样,也是 ES6 为了操作对象而提供的新的 API。

Reflect 的作用:

  • 将 Object 语言内部的方法拿出来放到 Reflect 对象上,即从 Reflect 对象上拿 Ojbect 对象内部方法。现阶段这些方法同时存在 Object 和 Reflect 上,未来新的方法将只部署在 Reflect 上。
  • 原 Object 方法报错的情况,在 Reflect 上会返回 false。使得代码运行更稳定。
  • 让 Object 操作都变成函数行为。原 Object 操作有一些是命令式,比如 in 和 delete,Reflect 用 has() 和 deleteProperty() 替代。
  • Reflect 对象上的方法与 Proxy 行为一一对应,只要是 Proxy 上的方法就会对应地出现在Reflect上。这可以使得两种对象相互配合完成默认的行为,作为修改行为的基础。即,不管 Proxy 如何修改默认行为,你总可以在 Reflect 上获取默认行为。

辅助说明示例:

2.某些Object方法调用可能会抛出异常,在Reflect上会返回false
// 老写法
try {
  Object.defineProperty(target, property, attributes);
  // success
} catch (e) {
  // failure
}

// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
  // success
} else {
  // failure
}
3.将命令式操作改成函数行为
// 老写法
'assign' in Object // true

// 新写法
Reflect.has(Object, 'assign') // true
4.与Proxy配合,获取对象的一些默认行为
var loggedObj = new Proxy(obj, {
  get(target, name) {
    console.log('get', target, name);
    return Reflect.get(target, name);
  },
  deleteProperty(target, name) {
    console.log('delete' + name);
    return Reflect.deleteProperty(target, name);
  },
  has(target, name) {
    console.log('has' + name);
    return Reflect.has(target, name);
  }
});

Reflect对象方法

一共有13个静态方法:

Reflect.get(target, name, receiver)           查找并返回target对象的name属性,如果没有该属性,则返回undefined。
Reflect.set(target, name, value, receiver)    设置target对象的name属性等于value
Reflect.defineProperty(target, name, desc)    基本等同于Object.defineProperty,用来为对象定义属性。未来,后者会被逐渐废除,请从现在开始就使用Reflect.defineProperty代替它。
Reflect.deleteProperty(target, name)          等同于delete obj[name],删除对象的属性
Reflect.has(target, name)                     对应name in obj里面的in运算符,判断属性是否存在于对象中
Reflect.construct(target, args)               等同于new target(...args),这提供了一种不使用new,来调用构造函数的方法。
Reflect.ownKeys(target)                       用于返回对象的所有属性,基本等同于Object.getOwnPropertyNames与Object.getOwnPropertySymbols之和。
Reflect.isExtensible(target)                  对应Object.isExtensible,返回一个布尔值,表示当前对象是否可扩展。
Reflect.preventExtensions(target)             对应Object.preventExtensions方法,用于让一个对象变为不可扩展。它返回一个布尔值,表示是否操作成功。
Reflect.getOwnPropertyDescriptor(target, name) 基本等同于Object.getOwnPropertyDescriptor,用于得到指定属性的描述对象,将来会替代掉后者。
Reflect.getPrototypeOf(target)                用于读取对象的__proto__属性,对应Object.getPrototypeOf(obj)。
Reflect.setPrototypeOf(target, prototype)     用于设置目标对象的原型(prototype),对应Object.setPrototypeOf(obj, newProto)方法,返回布尔值,表示是否设置成功。
Reflect.apply(target, thisArg, args)          用于绑定this对象后执行给定函数

应用场景举例:

  • 可以实现观察者模式,即观察数据的变化,一旦发生变化,自动执行对应的函数。

Guess you like

Origin www.cnblogs.com/V587Chinese/p/12132926.html