JavaScript对象、函数和类

JavaScript是应用事件编程

javascript是通过单线程来执行,当有事件发生,这个线程不一定有时间,需要一个机制让产生新事件等一等。这个机制就是Eventloop,从代码的角度看,所有的逻辑都是通过七七八八的“异步回调”来完成的;而从程序员思维方式的角度看,以往基于线程的编程,变成了事件驱动的编程。

  对于逻辑的触发,基于线程编程需要不断地由监视线程去查询被监视线程的某一个状态,如果状态满足某个条件,则触发相应的逻辑;而事件驱动则通过事件处理器,在事件发生时执行挂载的回调逻辑.(pull与push)

   基于线程的方式可以阻塞线程,等待时间或某个条件满足后再继续执行;而事件驱动则相反,发送一条消息后无阻塞等待回调的发生。阻塞线程的方式对资源的消耗往往更加显著,因为无论是否执行线程都被占用,但从人直观理解的角度来说,代码更直白,更符合人对顺序执行的理解;

而事件驱动的方式则相反,资源消耗上更优,但是代码的执行顺序,以及由此产生的系统状态判断变得难以预知。

 

一、Javascript对象

JavaScript本身就是面向对象的,JavaScript标准对基于对象的定义:语言和宿主的基础设施由对象来提供,并且JavaScript程序即是一系列互相通讯的对象集合。

认识对象的起源:人类思维模式:我们总是先认识到某一个苹果能吃(这里的某一个苹果就是一个对象),继而认识到所有的苹果都可以吃(这里的所有苹果,就是一个),再到后来我们才能意识到三个苹果和三个梨之间的联系,进而产生数字“3”()的概念。 

参考Grandy Booch《面向对象分析与设计》)。总结来看,对象有如下几个特点。

  • 对象具有唯一标识性:即使完全相同的两个对象,也并非同一个对象。
  • 对象有状态:对象具有状态,同一对象可能处于不同状态之下。
  • 对象具有行为:即对象的状态,可能因为它的行为产生变迁。

在 JavaScript中,将状态和行为统一抽象为“属性”。在实现了对象基本特征的基础上, JavaScript中对象独有的特色是:对象具有高度的动态性,这是因为JavaScript赋予了使用者在运行时为对象添改状态和行为的能力。

对JavaScript来说,属性并非只是简单的名称和值,JavaScript用一组特征(attribute)来描述属性(property)。

1)数据属性

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

2)访问器(getter/setter)属性,它也有四个特征。

  • getter:函数或undefined,在取属性值时被调用。
  • setter:函数或undefined,在设置属性值时被调用。
  • enumerable:决定for in能否枚举该属性。
  • configurable:决定该属性能否被删除或者改变特征值。
  var o = { a: 1 };
    Object.defineProperty(o, "b", {value: 2, writable: false, enumerable: false, configurable: true});
    //a和b都是数据属性,但特征值变化了
    Object.getOwnPropertyDescriptor(o,"a"); // {value: 1, writable: true, enumerable: true, configurable: true}
    Object.getOwnPropertyDescriptor(o,"b"); // {value: 2, writable: false, enumerable: false, configurable: true}
    o.b = 3;
    console.log(o.b); // 2

2、 Javascript采用了“原型”编程,也是面向对象编程的一种方式,没有 class 化的,直接使用对象。

基于类的语言提倡使用一个关注分类和类之间关系的开发模型。与此相对,原型编程看起来提倡程序员关注一系列对象实例的行为,而之后才关心如何将这些对象划分到最近的使用方式相似的原型对象,而不是分成类。

每个对象都有一个 __proto__ 的属性,这个就是“原型”。注意区分__proto__和prototype

注意:(1)__proto__ 主要是安放在一个实际的对象中,用它来产生一个链接,一个原型链连,用于寻找方法名或属性,等等.

           (2)prototype 是用 new 来创建一个对象时构造 __proto__ 用的。它是一个只属于 function 的属性

__proto__ 是所有对象用于链接原型的一个指针,而 prototype 则是 Function 对象的属性,其主要是用来当需要 new 一个对象时让 __proto__ 指针所指向的地方。 对于超级对象 Function 而言, Function.__proto__ 就是 Function.prototype

3、JavaScript中的对象分类

宿主对象(host Objects):由JavaScript宿主环境提供的对象,它们的行为完全由宿主环境决定。前端最熟悉的无疑是浏览器环境中的宿主,全局对象window上的属性

内置对象(Built-in Objects):由JavaScript语言提供的对象。固有对象(Intrinsic Objects );

原生对象(Native Objects);能够通过语言本身的构造器创建的对象称作原生对象,通过这些构造器,我们可以用new运算创建新的对象,几乎所有这些构造器的能力都是无法用纯JavaScript代码实现的,它们也无法用class/extend语法来继承。

 

 普通对象(Ordinary Objects):由{}语法、Object构造器或者class关键字定义类创建的对象,它能够被原型继承。

 二、函数

 

  JavaScript的函数是一等公民,如何理解这点呢。

函数可以不依附于任何类或对象等实体而独立存在,它可以单独作为参数、变量或返回值在程序中传递。

以往我们只能先指定宿主对象,再来调用函数;现在可以反过来,先指定函数,再来选择宿主对象,完成调用。请注意,函数的调用必须要有宿主对象,如果你使用 null 或者 undefined 这样不存在的对象,window 会取而代之,被指定为默认的宿主对象

 

 

通过获取funtion属性,看出prototype是function对象重要的属性,存储了Fuction的原型对象,prototype属性的值类型是对象,prototype值下有的constructor是属性,constructor 是一个比较特殊的属性,它指向构造函数(类)本身。(闭环)

caller:返回调用者,全局作用域返回是null  

arguments:是已经废弃的标准, 现在推荐的做法是使用函数内部可用的 arguments 对象来访问函数的实参。

length:是形参的个数

 

function f() {}
console.log(Object.getOwnPropertyNames(f)); // [ 'length', 'name', 'arguments', 'caller', 'prototype' ]

  

用户用function关键字创建的函数必定同时是函数和构造器。

function f(){
    return 1;
}
var v = f(); //把f作为函数调用
var o = new f(); //把f作为构造器调用

 new 运算接受一个构造器和一组调用参数,实际上做了几件事:

  • 构造器的 prototype 属性(注意与私有字段[[prototype]]的区分)为原型,创建新对象;
  • 将 this 和调用参数传给构造器,执行;
  • 如果构造器返回的是对象,则返回,否则返回第一步创建的对象。

new 这样的行为,试图让函数对象在语法上跟类变得相似,但是,它客观上提供了两种方式,一是在构造器中添加属性 (在实例方法上),二是在构造器的 prototype 属性上添加属性

function Foo(y) {
  this.y = y;
}
 
// 修改 Foo 的 prototype,加入一个成员变量 x
Foo.prototype.x = 10;
 
// 修改 Foo 的 prototype,加入一个成员函数 calculate
Foo.prototype.calculate = function (z) {
  return this.x + this.y + z;
};
 
// 现在,我们用 Foo 这个原型来创建 b 和 c
var b = new Foo(20);
var c = new Foo(30);
 
// 调用原型中的方法,可以得到正确的值
b.calculate(30); // 60
c.calculate(40); // 80

内存中的布局是怎么样的呢

 Foo.prototype 自动创建了一个属性 constructor这是一个指向函数自己的一个 reference。这样一来,

对于实例 b 或 c 来说,就能访问到这个继承的 constructor 了

 

console.log("Foo属性", Object.getOwnPropertyNames(Foo)); //函数对象 [ 'length', 'name', 'arguments', 'caller', 'prototype' ]
console.log("Foo.prototype属性", Object.getOwnPropertyDescriptor(Foo, "prototype")); //{ value: Foo { x: 10, calculate: [Function] },
//writable: true, enumerable: false, configurable: false }

let proto = Object.getOwnPropertyDescriptor(Foo, "prototype").value;
console.log("Foo.prototype.value", Object.getOwnPropertyNames(proto)); // ['constructor', 'x', 'calculate' ]
console.log(
    "Foo.prototype.constructor",
    Object.getOwnPropertyDescriptor(proto, "constructor") //{ value: [Function: Foo], writable: true, enumerable: false, configurable: true }
);

let con = Object.getOwnPropertyDescriptor(proto, "constructor").value;
console.log("Foo.prototype.constructor.value", Object.getOwnPropertyNames(con)); //函数对象 [ 'length', 'name', 'arguments', 'caller', 'prototype' ]
console.log("Foo.prototype.constructor.value的prototype属性", Object.getOwnPropertyDescriptor(con, "prototype")); // { value: Foo { x: 10, calculate: [Function] },
// writable: true,
//     enumerable: false,
//     configurable: false
// }

 

不实例new,也就是普通的函数调用而已,所以若是函数本身没有返回值,普通的函数调用没有什么意义 
如果函数返回值为常规意义上的数值类型(Number、String、Boolean)时,new函数将会返回一个该函数的实例对象,
而如果函数返回一个引用类型(Object、Array、Function)时,则new函数与直接调用函数产生的结果相同。

三、 JavaScript类

类只能用new来创建,而不能使用()来做函数调用

ES6之后,函数分成三种,一是类,只可以做new运算,方法只可以调用(),一般函数同时可做new和调用运算  

我们对于类的概念完全是通过强大的函数特性来实现的,

理解this的行为

function Book(name) {
    console.log(this);
    this.name = name;
    return this;
}
new Book("Life"); // 打印 Book {}
Book("Life"); // 打印 Window { ... }
window.Book("Life") // 打印 Window { ... }

 

实际上,上述例子在使用 new 这个关键字的时候,JavaScript 引擎就帮我们做了这样几件事情。

第一件,创建一个 Book 的对象,我们把它叫做 x 吧。
第二件,绑定原型:x.proto = Book.prototype。
第三件,指定对象自己:this = x,并调用构造方法,相当于执行了 x.Book()。
第四件,对于构造器中的 return 语句,根据 typeof x === ‘object’ 的结果来决定它实际的返回:

  • 如果 return 语句返回基本数据类型(如 string、boolean 等),这种情况 typeof x 就不是“object”,那么 new 的时候构造器的返回会被强制指定为 x;
  • 如果 return 语句返回其它类型,即对象类型,这种情况 typeof x 就是“object”,那么 new 的时候会遵循构造器的实际 return 语句来返回。

 

如:Book1 的构造器返回一个基本数据类型的数值 1,new 返回的就是 Book1 的实例对象本身;而 Book2 的构造器返回一个非基本数值类型 [](数组),new 返回的就是这个数组了。

function Book1(name) {
    this.name = name;
    return 1;
}
console.log(new Book1("Life")); // 打印 Book1 {name: "Life"}
 
function Book2(name) {
    this.name = name;
    return [];
}
console.log(new Book2("Life")); // 打印 []

ES5 开始提供了严格模式(Strict Mode),可以让代码对一些可能造成不良后果的不严谨、有歧义的用法报错。

在实际项目中,我们应当开启严格模式,或是使用 TypeScript 这样的 JavaScript 超集等等替代方案。写 JavaScript 代码的时候,心中要非常明确自己使用 function 的目的,是创建一个类,是创建某个对象的方法,还是创建一个普通的函数,并且在命名的时候,根据项目的约定给予清晰明确的名字,看到名字就立即可以知道它是什么,而不需要联系上下文去推导,甚至猜测。

 

 

ES6中加入了新特性class,new跟function搭配的怪异行为终于可以退休了(虽然运行时没有改变),在任何场景,我都推荐使用ES6的语法来定义类,而令function回归原本的函数语义。

类的写法实际上也是由原型运行时来承载的,逻辑上JavaScript认为每个类是有共同原型的一组对象,类中定义的方法和属性则会被写在原型对象之上

 一些激进的观点认为,class关键字和箭头运算符可以完全替代旧的function关键字,它更明确地区分了定义函数和定义类两种意图

   我们大致可以认为,它们[[construct]]的执行过程如下:

  • 以 Object.protoype 为原型创建一个新对象;
  • 以新对象为 this,执行函数的[[call]];
  • 如果[[call]]的返回值是对象,那么,返回这个对象,否则返回第一步创建的新对象。 

不使用new运算符,尽可能找到获得对象的方法。

    • // 1. 利用字面量
      var a = [], b = {}, c = /abc/g
      // 2. 利用dom api
      var d = document.createElement('p')
      // 3. 利用JavaScript内置对象的api
      var e = Object.create(null)
      var f = Object.assign({k1:3, k2:8}, {k3: 9})
      var g = JSON.parse('{}')
      // 4.利用装箱转换
      var h = Object(undefined), i = Object(null), k = Object(1), l = Object('abc'), m = Object(true)

 

JavaScript 面向对象的知识:封装、继承以及多态。

在面向对象编程中,封装(Encapsulation)说的是一种通过接口抽象将具体实现包装并隐藏起来的方法。具体来说,封装的机制包括两大部分:

  • 限制对对象内部组件直接访问的机制;
  • 将数据和方法绑定起来,对外提供方法,从而改变对象状态的机制。

在静态强类型的语言,比如java在类中通过 private 或 public 这样的修饰符,能够实现对对象属性或方法不同级别的访问权限控制。但是,在 JavaScript 中并没有这样的关键字这样的 name 属性,其实相当于公有属性

function Book(name) {
    this.name = name;
}
console.log(new Book("Life").name);

并且利用闭包的特性,将 name 封装在 Book 类的对象中,你无法通过任何其它方法访问到私有属性 name 的值。

闭包简单说,就是引用了自由变量的函数。这里的关键是“自由变量”,其实这个自由变量,扮演的作用是为这个函数调用提供了一个“上下文”,

另外,引出一个重要概念,和闭包相对的,是一种称为“纯函数”(Pure Function)的东西,即函数不允许引用任何自由变量

function Book(name) {
    this.getName = () => {
        return name;
    };
    this.setName = (newName) => {
        name = newName;
    };
}
let book = new Book("Life");
book.setName("Time");
console.log(book.getName()); // Time
console.log(book.name); // 无法访问私有属性 name 的值

继承

1、通过原型进行继承

2、构造继承

function Base1(name) {
    this.name = name;
}
function Base2(type) {
    this.type = type;
}
function Child(name, type) {
    Base1.call(this, name); // 让 this 去调用 Base1,并传入参数 name
    Base2.call(this, type);
}
 
var c = new Child("Life", "book");
console.log(c.name); // "Life"
console.log(c instanceof Base1); // false
console.log(c instanceof Child); // true

 

 

获取全部JavaScript固有对象

我们从JavaScript标准中可以找到全部的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

我们使用广度优先搜索,查找这些对象所有的属性和Getter/Setter,就可以获得JavaScript中所有的固有对象。

var set = new Set();
var vo=[ eval,
    isFinite,
    isNaN];
var fo=[eval,isFinite,isNaN, parseFloat,
    parseInt,
    decodeURI,
    decodeURIComponent,
    encodeURI,
    encodeURIComponent];
var co=[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]
var no=[ Atomics,
    JSON,
    Math,
    Reflect]


var objects = [...vo,...fo,...co,...no];
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)){
            console.log("set>",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);
    }
}

 深度搜索

Array:concat,copyWithin,fill,find,findIndex,lastIndexOf,pop,push,reverse,shift,unshift,slice,sort,splice,includes,indexOf,join,keys,entries,values,forEach,filter,flat,flatMap,map,every,some,reduce,reduceRight,toLocaleString,toString,isArray,from,of,
 Date:toString,toDateString,toTimeString,toISOString,toUTCString,toUTCString,getDate,setDate,getDay,getFullYear,setFullYear,getHours,setHours,getMilliseconds,setMilliseconds,getMinutes,setMinutes,getMonth,setMonth,getSeconds,setSeconds,getTime,setTime,getTimezoneOffset,getUTCDate,setUTCDate,getUTCDay,getUTCFullYear,setUTCFullYear,getUTCHours,setUTCHours,getUTCMilliseconds,setUTCMilliseconds,getUTCMinutes,setUTCMinutes,getUTCMonth,setUTCMonth,getUTCSeconds,setUTCSeconds,valueOf,getYear,setYear,toJSON,toLocaleString,toLocaleDateString,toLocaleTimeString,now,parse,UTC,
 RegExp: exec,get dotAll,get flags,get global,get ignoreCase,get multiline,get source,get sticky,get unicode,compile,toString,test,get input,set input,get $_,set $_,get lastMatch,set lastMatch,get $&,set $&,get lastParen,set lastParen,get $+,set $+,get leftContext,set leftContext,get $`,set $`,get rightContext,set rightContext,get $',set $',get $1,set $1,get $2,set $2,get $3,set $3,get $4,set $4,get $5,set $5,get $6,set $6,get $7,set $7,get $8,set $8,get $9,set $9,
 Promise:then,catch,finally,all,race,resolve,reject,allSettled,
 Proxy:revocable,
 Map:get,set,has,delete,clear,entries,forEach,keys,get size,values,
 WeakMap:delete,get,set,has,
 Set:has,add,delete,clear,entries,forEach,get size,values,values,
 WeakSet:delete,has,add,
 Function:,,,,,apply,bind,call,toString,
 Boolean:toString,valueOf,
 String:anchor,big,blink,bold,charAt,charCodeAt,codePointAt,concat,endsWith,fontcolor,fontsize,fixed,includes,indexOf,italics,lastIndexOf,link,localeCompare,match,matchAll,normalize,padEnd,padStart,repeat,replace,search,slice,small,split,strike,sub,substr,substring,sup,startsWith,toString,trim,trimStart,trimStart,trimEnd,trimEnd,toLocaleLowerCase,toLocaleUpperCase,toLowerCase,toUpperCase,valueOf,fromCharCode,fromCodePoint,raw,
 Number:toExponential,toFixed,toPrecision,toString,valueOf,toLocaleString,isFinite,isInteger,isNaN,isSafeInteger,parseFloat,parseInt,
 Symbol:toString,valueOf,get description,for,keyFor,
 Object:__defineGetter__,__defineSetter__,hasOwnProperty,__lookupGetter__,__lookupSetter__,isPrototypeOf,propertyIsEnumerable,toString,valueOf,get __proto__,set __proto__,toLocaleString,assign,getOwnPropertyDescriptor,getOwnPropertyDescriptors,getOwnPropertyNames,getOwnPropertySymbols,is,preventExtensions,seal,create,defineProperties,defineProperty,freeze,getPrototypeOf,setPrototypeOf,isExtensible,isFrozen,isSealed,keys,entries,values,fromEntries,
 Error:toString,captureStackTrace,
 EvalError:toString,
 RangeError:toString,
 ReferenceError:toString,
 SyntaxError:toString,
 TypeError:toString,
 URIError:toString,
 ArrayBuffer:get byteLength,slice,isView,
 SharedArrayBuffer:get byteLength,slice,
 DataView:get buffer,get byteLength,get byteOffset,getInt8,setInt8,getUint8,setUint8,getInt16,setInt16,getUint16,setUint16,getInt32,setInt32,getUint32,setUint32,getFloat32,setFloat32,getFloat64,setFloat64,getBigInt64,setBigInt64,getBigUint64,setBigUint64,

 undefined:defineProperty,deleteProperty,apply,construct,get,getOwnPropertyDescriptor,getPrototypeOf,has,isExtensible,ownKeys,preventExtensions,set,setPrototypeOf,

  JavaScript的函数是一等公民,如何理解这点呢。

函数可以不依附于任何类或对象等实体而独立存在,它可以单独作为参数、变量或返回值在程序中传递。

以往我们只能先指定宿主对象,再来调用函数;现在可以反过来,先指定函数,再来选择宿主对象,完成调用。请注意,函数的调用必须要有宿主对象,如果你使用 null 或者 undefined 这样不存在的对象,window 会取而代之,被指定为默认的宿主对象

posted on 2019-09-11 10:07  dollymi  阅读(1499)  评论(0编辑  收藏  举报

导航