木灵鱼儿

木灵鱼儿

阅读:553

最后更新:2022/05/01/ 2:02:08

第二章 字面量与构造函数

前言

JavaScript中可以使用字面量的形式去声明对象,相比较使用构造函数去声明,更加准确且不容易出错,代码也更加简洁,这里讨论常见的类型中,为什么基于字面量的形式更加可取。

对象字面量

当我们考虑创建一个键-值对哈希表时(在其他语言被称为关联数组),我们可以从一个“空”对象开始,然后再根据需要向其添加所需要的内容,对于按需对象创建方式而言,对象字面量表示法是一种非常理想的选择方法。

var dog = {};

dog.name = "jingmao";

dog.getName = function() {
  return dog.name;
}

//删除和复写也非常简单明了
delete dog.name;

dog.getName = "null";

当然,JavaScript也并不要求必须是从空对象开始,对于字面量模式可以在创建时就向其添加需要的内容

var dog = {
  name: "jingmao",
  getName: function() {
    return dog.name;
  }
}

在这里,空对象并不是指真正的空,泛指没有添加自定义属性的对象,因为在JavaScript中,哪怕是最简单的{}对象;也会从原型继承到属性和方法。

构造函数的对象

通过构造函数也可以创建对象,在JavaScript中内置了Object构造函数,我们可以通过它来创建一个对象。

const dog = new Object();

dog.name = "jingmao";

dog.getName = function() {
  return dog.name;
}

使用该方式存在一个缺点,除了Objcet可能被复写外,相对于字面量,它存在作用域解析,它需要从作用域中找到Object构造函数,如果层级过深,会一直沿着作用域链查询,直到找到为止。

甚至于使用new Objcet()创建时,传入不同的值,会产生不同的结果。

var a = new Object();
console.log(a.constructor === Object); //true

var b = new Object(1);
console.log(b.constructor === Number); //true
console.log(typeof b === "number"); //true

var c = new Object("name");
console.log(c.constructor === String); //true
console.log(typeof c === "string"); //true

var d = new Object(true);
console.log(d.constructor === Boolean); //true
console.log(typeof d === "boolean"); //true

当传递给Object()构造函数的值是动态的,并且直到运行时才能确定值的类型,这种行为可能导致意料之外的结果,因此,使用new Objct()构造函数远远不如使用字面量带来的收益高。

自定义构造函数

除了内置的Object构造函数,我们还可以自定义构造函数来创建对象

var a = new Person("admin");
a.say();  //admin

上面代码中,我们new了一个名为Person的类,虽然语法上看上去和java一样,通过类去创建一个对象,但是需要知道的是,在ES5规范中,JavaScript中并没有类的概念,而ES6中的类,也仅仅是一个语法糖而已。

我们来看一下Person的实现

function Person(name) {
  this.name = name;
  this.say = function() {
    return this.name;
  }
}

当使用new操作符调用构造函数时,函数内部会发生以下情况:

  • 创建了一个空对象,并将this指向这个新建对象,同时还继承了该函数的原型
  • 执行构造函数内部代码,对新对象添加属性和方法
  • 在没有显式抛出对象的情况下默认抛出this

伪代码

function Person(name) {
  //新对象
  var this = {};
  //原型继承
  this.__proto__ = Person.prototype;
  //对新对象添加属性和方法,
  //Person.call(this,name);
  //call看不懂可以看成下面,因为new的时候,真的运行并不是函数体内,这是伪代码
  this.name = name;
  this.say = function() {
    return this.name;
  }

  //判断是否有显式return,且抛出的值不是非空对象,否则使用默认的隐式抛出this
  return this;
}

非空对象伪代码

function Person(name) {
  this.name = name;
  this.say = function() {
    return this.name;
  }

  return ""; //空字符
}

//最终抛出this
console.log(new Person("admin")); //{ name: "admin",say: function() {} }
function Person(name) {
  this.name = name;
  this.say = function() {
    return this.name;
  }

  return null; //null
}

//最终抛出this
console.log(new Person("admin")); //{ name: "admin",say: function() {} }

非空对象,return的值是无效的,最终都会抛出this。

反之我们则可以控制构造函数new时所抛出的对象。

function Person(name) {
    this.name = name;
    return [];
}

console.log(new Person("admin"));   //[]

在本例中为了简单,直接将say方法添加到this上了,它会导致每次new Person时,都会在内存中创建一个say函数,这种方法的效率显然是非常低下的,因为多个实例之间say方法并没有改变,既然都是相同的,更好的选择就是添加到Person的原型上,通过继承的方式拿到

Person.prototype.say = function() {
  return this.name;
}

强制使用new的模式

我们知道,构造函数也只是一个函数,只不过它是以new的方式去调用,但是这并不是强制的,如果我们不使用new去调用会发生什么?

function Person(name) {
    this.name = name;
}

const a = Person("admin");

console.log(a);  //undefined
console.log(window.name);   //admin

直接运行构造函数时,会导致this指向window对象(在浏览器中),此时我们相当于给window添加了自定义属性,这显然并不是我们想看到的,因为程序员众所周知的一个原则就是保证全局命名空间的整洁。

这个问题也在ECMAScript5的严格模式中得到解决,在严格模式中这种用法会直接抛出错误。因为this被赋值为undefined。

"use strict";

function Person(name) {
    this.name = name;   //运行到这报错
}

const a = Person("admin");

console.log(a);
console.log(window.name);

构造函数的命名约定

事实上为了避免构造函数被直接作为函数使用,我们都遵循了一个命名约定,就是开头字母大写,但是这只是一种建议,并不能保证正确的行为,用户仍然可以将其作为一个普通函数来调用,这显然会带来一些问题。

自调用构造函数

为了解决这个问题,我们需要增加一些判断,我们可以在构造函数里判断this是否为构造函数的实例,因为只有new操作符运行时,才会使得this是构造函数的实例。

function Person(name) {
    //判断
    if(!this instanceof Person) {
      return new Person(name);
    }
    
    this.name = name;   
}

当然还有另一种方式

function Person(name) {
    //判断
    if(!this instanceof arguments.callee) {
      return new Person(name);
    }
    
    this.name = name;   
}

当函数被调用时,会创建一个名为arguments的变量,其中包含了传递给函数的参数,同时还存有被调用的函数本身,它的这个属性就是callee;但是非常遗憾的是在严格模式下,该属性是被禁用的,因为非常危险。

数组字面量

数组的字面量非常简单,一个方括号包裹,内容之间通过逗号分割,因为数组是一个0开始的索引列表,为此没必要通过new操作符使得事情变得复杂。

数组也可以通过内置的Array构造函数来创建,但是显然字面量的方式更加可取。

var a = [];

var b = new Array();

数组构造函数的特殊性

不推荐使用Array构造函数的一个原因是参数带来的陷阱。当我们给构造函数传入一个数字参数是,这个参数并不会成为数组中的值,而是成为一个设定数组长度的参数。

var a = new Array(3);

console.log(a);  //[,,]
console.log(a.length); //3
console.log(a[0]);  //undefined

这可能并不是我们预期的结果,如果我们传入一个浮点数,那情况还能更糟糕

var a = new Array(3.14);  //Uncaught RangeError: invalid array length

直接提示不合法的范围长度。

为了避免产生这种错误,使用字面量的方式会使得程序更加安全。

判断一个“数组”是否为数组

当我们使用typeof去判断数组对象是,得到的是object

显然这种行为是有意义的,因为数组也是一个对象,但是对于错误排查却没有什么帮助,在ES5以前,常见的做法是通过Object.prototype.toString方法,从上下文对象抛出一个字符串[object Array],此时我们可以准确的判断该变量是否为一个数组。

ES5新增了一个Array.isArray()的方法用于判断,且支持度良好,ie9及以上和安卓全系都已支持。

var a = new Array();

console.log(Object.prototype.toString.call(a));  //[object Array]
console.log(Array.isArray(a));  //true

写一个小Polyfill

if (!Array.isArray) {
    Array.isArray = function(arg) {
        return Object.prototype.toString.call(arg) === '[object Array]';
    };
}

JSON

json它代表了JavaScript的数据传输格式,是一种轻量级的数据交换格式,且可以很方便的用于多种语言。

在json字符串中不能使用:函数、正则表达式

在ES5版本以前,JSON还没有被纳入标准,对于一个json格式的数据解析,可能往往会使用eval来进行处理,或者使用JSON.org的第三方库,甚至一些第三方框架,都有携带对json解析的方法,比如JQ的JQuery.parseJSON()

显然用eval是一个很危险的操作,如果还在旧版,可以使用第三方库,而在ES5后,可以使用内置的JSON对象进行解析。

JSON.stringify();

JSON.parse();

使用该方法除了方便以外,性能会更好,且安全性也相应高一些。

正则表达式

正则表达式也支持字面量和构造函数两种方式创建。

以匹配两个反斜杠\\为例:

const reg1 = /\\/g;   
const reg2 = new RegExp("\\\\","g");

当我们使用字面量形式创建时,除了简洁易懂外,对于反斜杠处理更加易读,当使用g全局匹配时,反斜杠虽然可以定义为转义,但是使用g之后可以视为一个普通字符。

而在构造函数时,就不得不对转义反斜杠进行再转义,导致4个反斜杠的产生,给人一种非常错乱的感觉。

当然构造函数也不是一无是处,某些场景中我们无法确定需要匹配的内容具体是什么,只能在运行时以字符串的方式创建,此时就需要构造函数了。

但是在大部分情况下,使用字面量形式的正则会更受欢迎,有助于我们编写简洁易懂的代码。

基本类型

JavaScript有5个基本类型:数字、字符串、布尔、null、undefined;除了null和undefined以外,其他三个都具有构造函数,或者我们可以换个称呼,称其为包装对象

我们举个例子来说明其不同:

var a = 100;
console.log(a); //100
console.log(typeof a);  //number

var b = new Number(100);
console.log(b); //100
console.log(typeof b);  //object

可以看到,通过包装对象看上去和原来的值一模一样,但是它的类型已经变成object了,所以没有必要不要使用包装对象。

包装对象也不是一无是处,有时候我们会需要一些特殊方法,使用字面量就无法直接调起使用。

Number包装对象直接使用的方法

  • toFixed
  • toExponential
100.toFixed(); //Uncaught SyntaxError: Invalid or unexpected token  无法直接使用
const a = 100;
a.toFixed();  //100  必须创建变量

//包装对象
new Number(100).toFixed();  //100

//或者
(100).toFixed();  //100

通常使用包装对象的原因之一,就是需要扩展对象或者持久化保存状态,这也是因为基本类型并不是对象导致的。

当我们不使用new来调用包装器时,包装器会通过传递给他的参数转换成一个基本类型的值,这种方式也常常用来处理类型转换。

typeof Number(1); //number
typeof Number("1"); //number

typeof String(1); //string
typeof String("1"); //string

typeof Boolean(1); //boolean
typeof Boolean(false); //boolean

错误对象

JavaScript中有一些内置的错误构造函数,比如:Error、SyntaxError、TypeError等等,通过这些构造函数创建的错误对象具有下列属性:

  • name 创建该对象的构造函数名称
  • message 创建错误对象时传递给构造函数的字符串

错误对象还有一些其他属性,比如发生错误的文件行号和文件名,这个由于不同浏览器实现不同,因此并不可靠。

错误对象还有一个搭配操作符throw,只有使用该操作时,js运行才会被中断。但是并不是一定得是错误对象才能使用throw,我们可以操作任何对象,利用这个特性我们可以创建一个自定义的错误对象。

try {
  throw {
    name: "自定义错误",
    message: "xxxxx",
    remedy: test, //指定一个处理该错误的特殊函数
  }
} catch (error) {
  error.remedy();  //特殊处理
}

错误类型的构造函数,即便不使用new操作符,其表现行为和带new操作符一致。

版权申明

本文系作者 @木灵鱼儿 原创发布在木灵鱼儿 - 有梦就能远航站点。未经许可,禁止转载。

关于作者

站点职位 博主
获得点赞 0
文章被阅读 553

相关文章