es5--语法专题笔记

2018-11-15  本文已影响0人  guyigg

数据类型的转换

JavaScript 是一种动态类型语言,变量没有类型限制,可以随时赋予任意值。

var x = y ? 1 : 'a';

上面代码中,变量x到底是数值还是字符串,取决于另一个变量y的值。ytrue时,x是一个数值;yfalse时,x是一个字符串。这意味着,x的类型没法在编译阶段就知道,必须等到运行时才能知道。

虽然变量的数据类型是不确定的,但是各种运算符对数据类型是有要求的。如果运算符发现,运算子的类型与预期不符,就会自动转换类型。比如,减法运算符预期左右两侧的运算子应该是数值,如果不是,就会自动将它们转为数值。

强制转换主要指使用Number()String()Boolean()三个函数,手动将各种类型的值,分别转换成数字、字符串或者布尔值。
使用Number函数,可以将任意类型的值转化成数值。分成两种情况讨论,一种是参数是原始类型的值,另一种是参数是对象。

/ 数值:转换后还是原来的值
Number(324) // 324

// 字符串:如果可以被解析为数值,则转换为相应的数值
Number('324') // 324

// 字符串:如果不可以被解析为数值,返回 NaN
Number('324abc') // NaN

// 空字符串转为0
Number('') // 0

// 布尔值:true 转成 1,false 转成 0
Number(true) // 1
Number(false) // 0

// undefined:转成 NaN
Number(undefined) // NaN

// null:转成0
Number(null) // 0

Number函数将字符串转为数值,要比parseInt函数严格很多。基本上,只要有一个字符无法转成数值,整个字符串就会被转为NaN。另外,parseIntNumber函数都会自动过滤一个字符串前导和后缀的空格。

Number方法的参数是对象时,将返回NaN,除非是包含单个数值的数组。之所以会这样,是因为Number背后的转换规则比较复杂。

第一步,调用对象自身的valueOf方法。如果返回原始类型的值,则直接对该值使用Number函数,不再进行后续步骤。
第二步,如果valueOf方法返回的还是对象,则改为调用对象自身的toString方法。如果toString方法返回原始类型的值,则对该值使用Number函数,不再进行后续步骤。
第三步,如果toString方法返回的是对象,就报错。

var obj = {x: 1};
Number(obj) // NaN

// 等同于
if (typeof obj.valueOf() === 'object') {
  Number(obj.toString());
} else {
  Number(obj.valueOf());
}

上面代码中,Number函数将obj对象转为数值。背后发生了一连串的操作,首先调用obj.valueOf方法, 结果返回对象本身;于是,继续调用obj.toString方法,这时返回字符串[object Object],对这个字符串使用Number函数,得到NaN。如果toString方法返回的不是原始类型的值,结果就会报错。

var obj = {
  valueOf: function () {
    return {};
  },
  toString: function () {
    return {};
  }
};
Number(obj)
// TypeError: Cannot convert object to primitive value

Number({
  valueOf: function () {
    return 2;
  }
})
// 2

Number({
  toString: function () {
    return 3;
  }
})
// 3

Number({
  valueOf: function () {
    return 2;
  },
  toString: function () {
    return 3;
  }
})
// 2

从上例还可以看到,valueOftoString方法,都是可以自定义的。上面代码对三个对象使用Number函数。第一个对象返回valueOf方法的值,第二个对象返回toString方法的值,第三个对象表示valueOf方法先于toString方法执行。

String函数可以将任意类型的值转化成字符串,转换规则如下。

String({a: 1}) // "[object Object]"
String([1, 2, 3]) // "1,2,3"

String方法背后的转换规则,与Number方法基本相同,只是互换了valueOf方法和toString方法的执行顺序。

  1. 先调用对象自身的toString方法。如果返回原始类型的值,则对该值使用String函数,不再进行以下步骤。
  2. 如果toString方法返回的是对象,再调用原对象的valueOf方法。如果valueOf方法返回原始类型的值,则对该值使用String函数,不再进行以下步骤。
  3. 如果valueOf方法返回的是对象,就报错。
String({
  toString: function () {
    return 3;
  }
})
// "3"

String({
  valueOf: function () {
    return 2;
  }
})
// "[object Object]"

String({
  valueOf: function () {
    return 2;
  },
  toString: function () {
    return 3;
  }
})
// "3"

上面是通过自定义toString方法,改变返回值的例子。上面代码对三个对象使用String函数。第一个对象返回toString方法的值(数值3),第二个对象返回的还是toString方法的值([object Object]),第三个对象表示toString方法先于valueOf方法执行。

Boolean函数可以将任意类型的值转为布尔值。它的转换规则相对简单:除了以下五个值的转换结果为false,其他的值全部为true。注意,所有对象(包括空对象)的转换结果都是true,甚至连false对应的布尔对象new Boolean(false)也是true

undefined、null、-0或+0、NaN、‘ ’(空字符串)为false

Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean(NaN) // false
Boolean('') // false

自动转换,它是以强制转换为基础的。遇到以下三种情况时,JavaScript 会自动转换数据类型,即转换是自动完成的,用户不可见。
第一种情况,不同类型的数据互相运算。
第二种情况,对非布尔值类型的数据求布尔值。
第三种情况,对非数值类型的值使用一元运算符(即+-)。

第一种情况
123 + 'abc' // "123abc"

第二种情况
if ('abc') {
  console.log('hello')
}  // "hello"

第三种情况
+ {foo: 'bar'} // NaN
- [1, 2, 3] // NaN

自动转换的规则是这样的:预期什么类型的值,就调用该类型的转换函数。比如,某个位置预期为字符串,就调用String函数进行转换。如果该位置即可以是字符串,也可能是数值,那么默认转为数值。

JavaScript 遇到预期为布尔值的地方(比如if语句的条件部分),就会将非布尔值的参数自动转换为布尔值。系统内部会自动调用Boolean函数。下面两种写法,有时也用于将一个表达式转为布尔值。它们内部调用的也是Boolean函数。

// 写法一
expression ? true : false

// 写法二
!! expression

JavaScript 遇到预期为字符串的地方,就会将非字符串的值自动转为字符串。具体规则是,先将复合类型的值转为原始类型的值,再将原始类型的值转为字符串。

字符串的自动转换,主要发生在字符串的加法运算时。当一个值为字符串,另一个值为非字符串,则后者转为字符串。

'5' + 1 // '51'
'5' + true // "5true"
'5' + false // "5false"
'5' + {} // "5[object Object]"
'5' + [] // "5"
'5' + function (){} // "5function (){}"
'5' + undefined // "5undefined"
'5' + null // "5null"

JavaScript 遇到预期为数值的地方,就会将参数值自动转换为数值。系统内部会自动调用Number函数。除了加法运算符(+)有可能把运算子转为字符串,其他运算符都会把运算子自动转成数值。

'5' - '2' // 3
'5' * '2' // 10
true - 1  // 0
false - 1 // -1
'1' - 1   // 0
'5' * []    // 0
false / '5' // 0
'abc' - 1   // NaN
null + 1 // 1
undefined + 1 // NaN

注意:null转为数值时为0,而undefined转为数值时为NaN

错误处理机制

JavaScript 解析或运行时,一旦发生错误,引擎就会抛出一个错误对象。JavaScript 原生提供Error构造函数,所有抛出的错误都是这个构造函数的实例。

var err = new Error('出错了');
err.message // "出错了"

上面代码中,我们调用Error构造函数,生成一个实例对象errError构造函数接受一个参数,表示错误提示,可以从实例的message属性读到这个参数。抛出Error实例对象以后,整个程序就中断在发生错误的地方,不再往下执行。

JavaScript 语言标准只提到,Error实例对象必须有message属性,表示出错时的提示信息,没有提到其他属性。大多数 JavaScript 引擎,对Error实例还提供namestack属性,分别表示错误的名称和错误的堆栈,但它们是非标准的,不是每种实现都有。
message:错误提示信息
name:错误名称(非标准属性)
stack:错误的堆栈(非标准属性)

if (error.name) {
  console.log(error.name + ': ' + error.message);
}

function throwit() {
  throw new Error('');
}

function catchit() {
  try {
    throwit();
  } catch(e) {
    console.log(e.stack); // print stack trace
  }
}
catchit()
// Error
//    at throwit (~/examples/throwcatch.js:9:11)
//    at catchit (~/examples/throwcatch.js:3:9)
//    at repl:1:5

上面代码中,错误堆栈的最内层是throwit函数,然后是catchit函数,最后是函数的运行环境。

Error实例对象是最一般的错误类型,在它的基础上,JavaScript 还定义了其他6种错误对象。也就是说,存在Error的6个派生对象。
SyntaxError对象是解析代码时发生的语法错误。下面代码的错误,都是在语法解析阶段就可以发现,所以会抛出SyntaxError。第一个错误提示是“token 非法”,第二个错误提示是“字符串不符合要求”。

// 变量名错误
var 1a;
// Uncaught SyntaxError: Invalid or unexpected token

// 缺少括号
console.log 'hello');
// Uncaught SyntaxError: Unexpected string

ReferenceError对象是引用一个不存在的变量时发生的错误。另一种触发场景是,将一个值分配给无法分配的对象,比如对函数的运行结果或者this赋值。

// 使用一个不存在的变量
unknownVariable
// Uncaught ReferenceError: unknownVariable is not defined

// 等号左侧不是变量
console.log() = 1
// Uncaught ReferenceError: Invalid left-hand side in assignment

// this 对象不能手动赋值
this = 1
// ReferenceError: Invalid left-hand side in assignment

上面代码对函数console.log的运行结果和this赋值,结果都引发了ReferenceError错误。

RangeError对象是一个值超出有效范围时发生的错误。主要有几种情况,一是数组长度为负数,二是Number对象的方法参数超出范围,以及函数堆栈超过最大值。

// 数组长度不得为负数
new Array(-1)
// Uncaught RangeError: Invalid array length

TypeError对象是变量或参数不是预期类型时发生的错误。比如,对字符串、布尔值、数值等原始类型的值使用new命令,就会抛出这种错误,因为new命令的参数应该是一个构造函数。

new 123
// Uncaught TypeError: number is not a func

var obj = {};
obj.unknownMethod()
// Uncaught TypeError: obj.unknownMethod is not a function

上面代码的第二种情况,调用对象不存在的方法,也会抛出TypeError错误,因为obj.unknownMethod的值是undefined,而不是一个函数。

URIError对象是 URI 相关函数的参数不正确时抛出的错误,主要涉及encodeURI()decodeURI()encodeURIComponent()decodeURIComponent()escape()unescape()这六个函数。

decodeURI('%2')
// URIError: URI malformed

eval函数没有被正确执行时,会抛出EvalError错误。该错误类型已经不再使用了,只是为了保证与以前代码兼容,才继续保留。

以上这6种派生错误,连同原始的Error对象,都是构造函数。开发者可以使用它们,手动生成错误对象的实例。这些构造函数都接受一个参数,代表错误提示信息(message)。

var err1 = new Error('出错了!');
var err2 = new RangeError('出错了,变量超出有效范围!');
var err3 = new TypeError('出错了,变量类型无效!');

err1.message // "出错了!"
err2.message // "出错了,变量超出有效范围!"
err3.message // "出错了,变量类型无效!"

除了 JavaScript 原生提供的七种错误对象,还可以定义自己的错误对象。

function UserError(message) {
  this.message = message || '默认信息';
  this.name = 'UserError';
}

UserError.prototype = new Error();
UserError.prototype.constructor = UserError;

new UserError('这是自定义的错误!');

上面代码自定义一个错误对象UserError,让它继承Error对象。然后,就可以生成这种自定义类型的错误了。

throw语句的作用是手动中断程序执行,抛出一个错误。下面代码中,如果变量x小于等于0,就手动抛出一个错误,告诉用户x的值不正确,整个程序就会在这里中断执行。可以看到,throw抛出的错误就是它的参数,这里是一个Error实例。

if (x <= 0) {
  throw new Error('x 必须为正数');
}
// Uncaught ReferenceError: x is not defined

throw也可以抛出自定义错误。

function UserError(message) {
  this.message = message || '默认信息';
  this.name = 'UserError';
}

throw new UserError('出错了!');
// Uncaught UserError {message: "出错了!", name: "UserError"}

上面代码中,throw抛出的是一个UserError实例。实际上,throw可以抛出任何类型的值。也就是说,它的参数可以是任何值。对于 JavaScript 引擎来说,遇到throw语句,程序就中止了。引擎会接收到throw抛出的信息,可能是一个错误实例,也可能是其他类型的值。

// 抛出一个字符串
throw 'Error!';
// Uncaught Error!

// 抛出一个数值
throw 42;
// Uncaught 42

// 抛出一个布尔值
throw true;
// Uncaught true

// 抛出一个对象
throw {
  toString: function () {
    return 'Error!';
  }
};
// Uncaught {toString: ƒ}

一旦发生错误,程序就中止执行了。JavaScript 提供了·try...catch·结构,允许对错误进行处理,选择是否往下执行。

try {
  throw new Error('出错了!');
} catch (e) {
  console.log(e.name + ": " + e.message);
  console.log(e.stack);
}
// Error: 出错了!
//   at <anonymous>:3:9
//   ...

上面代码中,try代码块抛出错误(上例用的是throw语句),JavaScript 引擎就立即把代码的执行,转到catch代码块,或者说错误被catch代码块捕获了。catch接受一个参数,表示try代码块抛出的值。catch代码块之中,还可以再抛出错误,甚至使用嵌套的try...catch结构。

var n = 100;

try {
  throw n;
} catch (e) {
  if (e <= 50) {
    // ...
  } else {
    throw e;
  }
}
// Uncaught 100

为了捕捉不同类型的错误,catch代码块之中可以加入判断语句。下面代码中,catch捕获错误之后,会判断错误类型(EvalError还是RangeError),进行不同的处理。

try {
  foo.bar();
} catch (e) {
  if (e instanceof EvalError) {
    console.log(e.name + ": " + e.message);
  } else if (e instanceof RangeError) {
    console.log(e.name + ": " + e.message);
  }
  // ...
}

try...catch结构允许在最后添加一个finally代码块,表示不管是否出现错误,都必需在最后运行的语句。

function cleansUp() {
  try {
    throw new Error('出错了……');
    console.log('此行不会执行');
  } finally {
    console.log('完成清理工作');
  }
}

cleansUp()
// 完成清理工作
// Uncaught Error: 出错了……
//    at cleansUp (<anonymous>:3:11)
//    at <anonymous>:10:1

上面代码中,由于没有catch语句块,一旦发生错误,代码就会中断执行。中断执行之前,会先执行finally代码块,然后再向用户提示报错信息。

例一
function idle(x) {
  try {
    console.log(x);
    return 'result';
  } finally {
    console.log('FINALLY');
  }
}
idle('hello')
// hello
// FINALLY
例二
var count = 0;
function countUp() {
  try {
    return count;
  } finally {
    count++;
  }
}
countUp()
// 0
count
// 1

上面代码例一中,try代码块没有发生错误,而且里面还包括return语句,但是finally代码块依然会执行。而且,这个函数的返回值还是result。上面例二说明,return语句的执行是排在finally代码之前,只是等finally代码执行完毕后才返回。return语句里面的count的值,是在finally代码块运行之前就获取了。

openFile();
try {
  writeFile(Data);
} catch(e) {
  handleError(e);
} finally {
  closeFile();
}
//执行顺序
function f() {
  try {
    console.log(0);
    throw 'bug';
  } catch(e) {
    console.log(1);
    return true; // 这句原本会延迟到 finally 代码块结束再执行
    console.log(2); // 不会运行
  } finally {
    console.log(3);
    return false; // 这句会覆盖掉前面那句 return
    console.log(4); // 不会运行
  }

  console.log(5); // 不会运行
}

var result = f();
// 0
// 1
// 3

result
// false

上面代码首先打开一个文件,然后在try代码块中写入文件,如果没有发生错误,则运行finally代码块关闭文件;一旦发生错误,则先使用catch代码块处理错误,再使用finally代码块关闭文件。

编程风格

代码缩进;建议总是使用大括号明确区块;圆括号;建议总是在行尾加上分号(forwhiledo...while不需要分号);全局变量建议用大写来区分;尽量不要是用with语句;相等和严格相等尽量使用严格相等;建议不要将不同目的的语句,合并成一行;建议自增(++)和自减(--)运算符尽量使用+=-=代替;尽量不要使用switch...case,用其他方式来代替。

function doAction(action) {
  switch (action) {
    case 'hack':
      return 'hack';
      break;
    case 'slash':
      return 'slash';
      break;
    case 'run':
      return 'run';
      break;
    default:
      throw new Error('Invalid action.');
  }
}
//可以改写成对象结构来代替
function doAction(action) {
  var actions = {
    'hack': function () {
      return 'hack';
    },
    'slash': function () {
      return 'slash';
    },
    'run': function () {
      return 'run';
    }
  };

  if (typeof actions[action] !== 'function') {
    throw new Error('Invalid action.');
  }
  return actions[action]();
}

console 对象与控制台

console对象是 JavaScript 的原生对象,它有点像 Unix 系统的标准输出stdout和标准错误stderr,可以输出各种信息到控制台,并且还提供了很多有用的辅助方法。console的常见用途有两个:调试程序,显示网页代码运行时的错误信息;提供了一个命令行接口,用来与网页代码互动。

打开开发者工具以后,顶端有多个面板。

console.log方法用于在控制台输出信息。它可以接受一个或多个参数,将它们连接起来输出。
如果第一个参数是格式字符串(使用了格式占位符),console.log方法将依次用后面的参数替换占位符,然后再进行输出。console.log方法支持以下占位符,不同类型的数据必须使用对应的占位符。
%s 字符串、%d 整数、%i 整数、%f浮点数、%o 对象的链接、%c CSS 格式字符串

console.log(' %s + %s = %s', 1, 1, 2)
//  1 + 1 = 2

var number = 11 * 9;
var color = 'red';
console.log('%d %s balloons', number, color);
// 99 red balloons

//使用%c占位符时,对应的参数必须是 CSS 代码,用来对输出内容进行 CSS 渲染。
console.log(
  '%cThis text is styled!',
  'color: red; background: yellow; font-size: 24px;'
)

console.infoconsole.log方法的别名,用法完全一样。只不过console.info方法会在输出信息的前面,加上一个蓝色图标。

console.debug方法与console.log方法类似,会在控制台输出调试信息。但是,默认情况下,console.debug输出的信息不会显示,只有在打开显示级别在verbose的情况下,才会显示。console.warn方法和console.error方法也是在控制台输出信息,它们与log方法的不同之处在于,warn方法输出信息时,在最前面加一个黄色三角,表示警告;error方法输出信息时,在最前面加一个红色的叉,表示出错。同时,还会高亮显示输出文字和错误发生的堆栈。其他方面都一样。对于某些复合类型的数据,console.table方法可以将其转为表格显示。

var languages = [
  { name: "JavaScript", fileExtension: ".js" },
  { name: "TypeScript", fileExtension: ".ts" },
  { name: "CoffeeScript", fileExtension: ".coffee" }
];
console.table(languages);

console.count方法用于计数,输出它被调用了多少次。该方法可以接受一个字符串作为参数,作为标签,对执行次数进行分类。

function greet(user) {
  console.count();
  return 'hi ' + user;
}
greet('bob')
//  : 1
// "hi bob"
greet('alice')
//  : 2
// "hi alice"
greet('bob')
//  : 3
// "hi bob"

function greet(user) {
  console.count(user);
  return "hi " + user;
}
greet('bob')
// bob: 1
// "hi bob"
greet('alice')
// alice: 1
// "hi alice"
greet('bob')
// bob: 2
// "hi bob"

console.dir()console.dirxml()方法用来对一个对象进行检查(inspect),并以易于阅读和打印的格式显示。dir方法对于输出 DOM 对象非常有用,因为会显示 DOM 对象的所有属性。dirxml方法主要用于以目录树的形式,显示 DOM 节点。如果参数不是 DOM 节点,而是普通的 JavaScript 对象,console.dirxml等同于console.dir

console.dir(document.body);
console.dirxml(document.body);

console.time()console.timeEnd()这两个方法用于计时,可以算出一个操作所花费的准确时间。time方法表示计时开始,timeEnd方法表示计时结束。它们的参数是计时器的名称。调用timeEnd方法之后,控制台会显示“计时器名称: 所耗费的时间”。

console.time('Array initialize');
var array= new Array(1000000);
for (var i = array.length - 1; i >= 0; i--) {
  array[i] = new Object();
};
console.timeEnd('Array initialize');
// Array initialize: 1914.481ms

console.groupconsole.groupEnd这两个方法用于将显示的信息分组。它只在输出大量信息时有用,分在一组的信息,可以用鼠标折叠/展开。console.groupCollapsed方法与console.group方法很类似,唯一的区别是该组的内容,在第一次显示时是收起的(collapsed),而不是展开的。

console.trace方法显示当前执行的代码在堆栈中的调用路径。console.clear方法用于清除当前控制台的所有输出,将光标回置到第一行。如果用户选中了控制台的“Preserve log”选项,console.clear方法将不起作用。

浏览器控制台中,除了使用console对象,还可以使用一些控制台自带的命令行方法。(了解)。

debugger语句主要用于除错,作用是设置断点。如果有正在运行的除错工具,程序运行到debugger语句时会自动停下。如果没有除错工具,debugger语句不会产生任何结果,JavaScript 引擎自动跳过这一句。

for(var i = 0; i < 5; i++){
  console.log(i);
  if (i === 2) debugger;
}
上一篇下一篇

猜你喜欢

热点阅读