重学前端(二)-你真的了解你JS的对象吗?

2,155 阅读12分钟

书接上文,开始重学前端(第二篇)

Object——对象

开篇之初我们先抛出几个问题?

  • 1、什么是面向对象?
  • 2、function 是一个对象吗?
  • 3、对象分为几类呢?
  • 4、什么是原型对象?
  • 6、构造函数到底是个什么玩意?
  • 7、new到底干了一件什么事?

回想一下这个这些问题你心中是否已有答案呢?在接下来的内容中,我们逐一共同学习!

正篇

灵魂质问?到底什么是js

JavaScript(简称“JS”) 是一种具有函数优先的轻量级,解释型或即时编译型的编程语言。虽然它是作为开发Web页面的脚本语言而出名的,但是它也被用到了很多非浏览器环境中,JavaScript 基于原型编程、多范式的动态脚本语言,并且支持面向对象、命令式和声明式(如函数式编程)风格。

百度是这样说的,这就不是人话,其实本质上js 是啥?

js就是专门编写网页交互行为的语言

那js是由什么组成的呢,简单来说就一句话

ECMAScript标准+ webAPI 那么我们今天要一起学习的就是ECMASciript中的-Object,他实际上是一个es的语言标准

什么是对象-Object?

对象其实有两个特点

  • 1、描述现实中一个具体事物的属性和功能的程序结构
  • 2、内存中同时存储多个数据和方法的一块存储空间。

既然对象是一个具体事物的属性和功能。那么,事物的属性会成为对象的属性事物的功能会成为对象的方法

什么是面向对象?

在程序中,都是先用对象封装一个事物的属性和功能。然后,再调用对象 的方法,来执行任务。这就是面向对象,其实在es6出来之前,js总是显得这么合群,其他语言该有的对象的结构,他是一个没沾上,知道es6横空出世,我们才有了类这个概念,面向对象也才算是正式打响!

对象的底层到底是什么?

由于是重学前端,我们就不讲怎么创建对象这种百度搜索能有100页答案的东西了。

我们来探究一下,对象的底层到底是什么? 咱研究底层之前,我们先来看看数组

数组

数组对象的作用是:使用单独的变量名来存储一系列的值。

数组呢,他还可以分类,分为普通数组,二维数组,hash数组,普通数组,和二维数组不在赘述了,讲讲这个hash数组

hash 数组

哈希数组,又称关联数组,顾名思义,他的数组元素是由[key:value]组成的

var myhash = new Array();
myhash['new'] = 'newval';
myhash['new2'] = 'newval_2';

上述代码就会产生如下结果:就是是哈希数组的数据结构

到这你是不是发现,我们的对象也能这么去赋值,和取值

如下图,我们发现我们用上述方法去给对象赋值和取值,也可以实现

由此得出结论:对象底层就是 hash 数组,只不过他在关联数组上有添加了许多包装属性,和方法,这样的结构就导致了,对象有这很多特性比如 对象具有高度的动态性,JavaScript给使用者在运行时为对象添改状态和行为的能力。(大佬的总结,我照抄)

对象的两类属性特征

在我们日常的认知中,对象只是简单的键值对,其实我们深究的时候发现,对象还提供了一些特征来描述我们的对象成员

1、描述数据属性的特征

  • value:就是属性的值。
  • writable:决定属性能否被赋值。
  • enumerable:决定for in能否枚举该属性。
  • configurable:决定该属性能否被删除或者改变特征值。

1、描述访问器属性的特征

  • getter:函数或undefined,在取属性值时被调用。
  • setter:函数或undefined,在设置属性值时被调用。
  • enumerable:决定for in能否枚举该属性。
  • configurable:决定该属性能否被删除或者改变特征值。

通常情况下,这些我们是用不上看不见的,我们只需要关心赋值和取值即可,那你说我非要用咋办?

我们可以通过内置的Object.getOwnPropertyDescripte来查看

  var actions = {
            b: 1,
            c: 2
        }
        var d = Object.getOwnPropertyDescriptor(actions, "b")
        console.log(d)

结果如下

如果想要修改我们可以通过内置的Object.defineProperty来修改,这也是vue能实现数据劫持的原理

 var value = { a: 1 };
        Object.defineProperty(value, "b", { value: 2, writable: false, enumerable: false, configurable: true });

        var obja = Object.getOwnPropertyDescriptor(value, "a");
        var objb = Object.getOwnPropertyDescriptor(value, "b");
        //我们发现赋值为3已经不能改了
        value.b = 3;
        console.log(value.b, obja, objb); // 2

那他又是怎么实现数据劫持的呢?别急一步步探究

        var value = { b: 1 };
        var tm = null;
        Object.defineProperty(value, "b",
            {
                get: function () {
                    console.log('我取值的时候被调用了一次')
                    return tm
                },
                set: function (a) {
                    console.log('我赋值的时候被调用一次')
                    tm = a
                }
            });
        //赋值
        value.b = 3;
        //取值
        console.log(value.b);

我们发现,其实vue的响应式核心就是 Object.defineProperty这个方法能监听到值的改变,然后去通知已经订阅的各个方法,进行相应的操作,来达到改变视图的目的

面向对象之继承

说起面向对象,继承必不可少,那么什么是继承呢?

用大白话解释:继承就是父对象的成员,子对象无需创建,就可直接使用

那么我们怎么继承呢?

原型对象实现继承

由于在es6出现之前,我们没有类的概念,我们的语言标准,就沿用了祖师爷发明的原型系统,虽然不是正统语言该有的样子,但也独领风骚,什么都长得像java还能叫js吗?

原型就是新对象持有一个放公用属性和方法的的引用的地方,注意并不真的去复制一个原型对象,而是使得新对象持有一个原型的引用,每个构造函数在出生的时候(constructor)都附送一个原型对象(prototype)

上述概念,就回答了什么叫做原型对象。这里又有一个老生常谈的名字,构造函数

构造函数

构造函数:专门定义一类对象统一结构的特殊函数。

构造函数和原型以及对象之间的关系如下图所示:

我们发现其实他们之间其实就是靠着新对象用双下划线 proto 继承原型对象,构造函数用 prototyoe引用原型对象

 function Person(name) {
            this.name = name;
        }
        // 通过构造函数的 Person 的 prototype 属性找到 Person 的原型对象
        Person.prototype.eat = function () {
            console.log("吃饭");
        }

        let Person1 = new Person("aaa", 24);
        let Person2 = new Person("bbb", 24);

        console.log(Person1.eat === Person2.eat)//发现相等
        console.log(Person1)
        console.log(Person2)

在上图中我们发现,他的双下划线proto中还有双下划线proto这就形成了链条状我们把它叫做原型链

理解上上述原理之后,我们就可以轻松发现,如果要实现当下流行的这些 构造函数继承、组合继承、原型继承、寄生式继承、寄生组合式继承 其实不过是改变他的.prototype指向,从而改变双下划线proto的指向而已(我是这样理解的,错误之处大佬指出),当然在es6大行其道的今天,我们一个class和extends 全部干掉了这些花里胡哨!

   class Animal {
            constructor(name) {
                this.name = name;
            }
            speak() {
                console.log(this.name + ' makes a noise.');
            }
        }
        class Dog extends Animal {
            constructor(name) {
                super(name); // call the super class constructor and pass in the name parameter
            }
            speak() {
                console.log(this.name + ' barks.');
            }
        }
        let d = new Dog('Mitzie');
        d.speak(); // Mitzie barks.

在了解玩这些概念之后,我们回答一个上面的问题,new到底干了一件什么事?

new到底干了一件什么事情?

我的理解这个new关键字其实干了四件事,也很好记忆

  • 创建一个空对象
  • 设置新对象的__proto__继承构造函数的原型对象
  • 用新对象调用构造函数,将构造函数中的 this,替换为空对象 构造函数会向空对象中添加新的属性和方法。
  • 将对象地址返回给 obj

上文中提到this指向问题在此梳理一下,几种情况

this指向

this的绑定规则总共有下面4种。

  • 1、默认绑、隐式绑定(严格/非严格模式)
  • 2、显式绑定
  • 3、new绑定
  • 4、箭头函数绑定

首先声明:this的确定是在运行时确定也就是调用时,调用位置就是函数在代码中被调用的位置(而不是声明的位置)

其实我的理解是this是在将这个函数压入执行环境栈的时候就已经被确定了,执行的时候只不过从已经确定的this,指向的地方去拿对象替代this(之前这个this的确定时间问题,被一个阿里大佬问到过,当时也是云里雾里,特地去网上查了很多资料也是各种版本,在这里说一下我的理解,不对之处请大佬指出)

默认绑定

默认指向调用这个方法的对象,如果没有对象去调用,那就不用想了,就指向全局,如果是严格模式,那就是undefined

function foo() {
    console.log( this.a );
}

var obj = {
    a: 2,
    foo: foo
};
//我们发现前面有对象调用
obj.foo(); // 2
 
 var obj = {
            b: function () {
                console.log(this);
            }
        }
        function b(b) {
        //发现前面没有调用对象调用,那就指向window
            b()
        }2
        b(obj.b)//window

显式绑定

通过call,apply,bind去绑定this,就叫做显示绑定,这些为啥能实现显示绑定,我就不在赘述,有兴趣可以去看我之前临摹的call,apply,bind的源码 代码抗击病毒之-大厂面试必考题总结

function foo() {
    console.log( this.a );
}
var obj = {
    a: 2
};

foo.call( obj ); // 2用call强制绑定了一下

new绑定

function foo(a) {
    this.a = a;
}

var bar = new foo(2); //在new的时候this被绑定到了这个新对象
console.log( bar.a ); // 2

箭头函数绑定

ES6新增一种特殊函数类型:箭头函数,箭头函数无法使用上述规则

var foo =()=> {
            console.log(this)
        }
        var a = {
            b: foo
        }
        //这时发现this无法被改变了
        a.b()//window

这种绑定,其实在声明的时候就已经被确定为当前上下文的作用域环境中的this(当时还是被这个阿里大佬问过,箭头函数的this在什么时候确定的,我的理解也是在压入环境栈的时时候,去确定作用域,然后在执行的时候从作用域环境中去拿到this,也就是声明的时候就被绑定,不对指出求指正)

对象的所有种类

我们都知道万物皆对象,但是其实在js中对象也分种类的,除了我们平常知道的普通对象之外我们还有宿主对象、内置对象接下来一一讲解

宿主对象(host Objects)

由JavaScript宿主环境提供的对象,它们的行为完全由宿主环境决定。

宿主对象就是我们的js运行在的地方他提供的对象,我们最熟悉不过的就是浏览器环境了, 我们的宿主对象就是window,这个window包含的内容千奇百怪一部分来自 JavaScript语言,一部分来自浏览器环境。

内置对象(Built-in Objects)

内置对象又包含固有对象、原生对象

固有对象(Intrinsic Objects )

固有对象是由标准规定,随着JavaScript运行时创建而自动创建的对象实例。

固有对象在任何JS代码执行前就已经被创建出来了,它们通常扮演者类似基础库的角色。我们常用的一些js方法其实就是固有对象

原生对象(Native Objects)

可以由用户通过Array、RegExp等内置构造器或者特殊语法创建的对 象。

我们把JavaScript中,能够通过语言本身的构造器创建的对象称作原生对象。在JavaScript标准中,提供了30 多个构造器,通过这些构造器,我们可以用new运算创建新的对象,所以我们把这些对象称作原生对象。

到这里,我们发现函数其实也是个对象,这就回答了,开头中提到的问题,下面我们来验证一下

 function foo() {

        }
        foo.b = 3
        console.log(foo)
        console.log(foo.b)//3

我们发现,我竟然能给函数赋值一个属性,这是只有对象才能有的特权啊,所以由此说明他也是一个对象

写在最后

系统的学习了一下对象到底是个什么玩意,并回答了上面的几个问。再次感谢极客大佬的重学前端,让我重新认识js,记录学习,不对之处,欢迎大佬指正!

最后,我们留下一个大佬的查询固有对象的代码 他列举了所有含有固有对象的js对象 三个值:

Infinity、NaN、undefined。

九个函数:

  • eval
  • isFinite
  • isNaN
  • parseFloat
  • parseInt
  • decodeURI
  • decodeURIComponent
  • encodeURI
  • encodeURIComponent

** 一些构造器:**

Array、Date、RegExp、Promise、Proxy、Map、WeakMap、Set、WeapSet、Function、Boolean、 String、Number、Symbol、Object、Error、EvalError、RangeError、ReferenceError、SyntaxError

TypeError

URIError、ArrayBuffer、SharedArrayBuffer、DataView、Typed Array、Float32Array、Float64Array、 Int8Array、Int16Array、Int32Array、UInt8Array、UInt16Array、UInt32Array、UInt8ClampedArray。

四个用于当作命名空间的对象:

Atomics JSON Math Reflect

 var set = new Set();
        var objects = [
            eval,
            isFinite,
            isNaN,
            parseFloat,
            parseInt,
            decodeURI,
            decodeURIComponent,
            encodeURI,
            encodeURIComponent,
            Array,
            Date,
            RegExp,
            Promise,
            Proxy,
            Map,
            WeakMap,
            Set,
            WeakSet,
            Function,
            Boolean,
            String,
            Number,
            Symbol,
            Object,
            Error,
            EvalError,
            RangeError,
            ReferenceError,
            SyntaxError,
            TypeError,
            URIError,
            ArrayBuffer,
            SharedArrayBuffer,
            DataView,
            Float32Array,
            Float64Array,
            Int8Array,
            Int16Array,
            Int32Array,
            Uint8Array,
            Uint16Array,
            Uint32Array,
            Uint8ClampedArray,
            Atomics,
            JSON,
            Math,
            Reflect];
        objects.forEach(o => set.add(o));
        for (var i = 0; i < objects.length; i++) {
            var o = objects[i]
            for (var p of Object.getOwnPropertyNames(o)) {
                var d = Object.getOwnPropertyDescriptor(o, p)
                if ((d.value !== null && typeof d.value === "object") || (typeof d.value === "function"))
                    if (!set.has(d.value))
                        set.add(d.value), objects.push(d.value);
                if (d.get)
                    if (!set.has(d.get))
                        set.add(d.get), objects.push(d.get);
                if (d.set)
                    if (!set.has(d.set))
                        set.add(d.set), objects.push(d.set);
            }
        }
        console.log(set)//441种