JS一些特殊知识点(三)

Error 实例对象

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

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

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

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

使用namemessage这两个属性,可以对发生什么错误有一个大概的了解。

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

stack属性用来查看错误发生时的堆栈。

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对象是解析代码时发生的语法错误。

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

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

上面代码的错误,都是在语法解析阶段就可以发现,所以会抛出SyntaxError。第一个错误提示是“token 非法”,第二个错误提示是“字符串不符合要求”。

ReferenceError 对象

ReferenceError对象是引用一个不存在的变量时发生的错误。

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

另一种触发场景是,将一个值分配给无法分配的对象,比如对函数的运行结果或者this赋值。

// 等号左侧不是变量
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 对象

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

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

TypeError 对象

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 对象

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

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

EvalError 对象

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;

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

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

throw 语句

throw语句的作用是手动中断程序执行,抛出一个错误。

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

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

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

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

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

上面代码中,throw抛出的是一个UserError实例。

实际上,throw可以抛出任何类型的值。也就是说,它的参数可以是任何值。

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

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

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

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

对于 JavaScript 引擎来说,遇到throw语句,程序就中止了。引擎会接收到throw抛出的信息,可能是一个错误实例,也可能是其他类型的值。

try…catch 结构

一旦发生错误,程序就中止执行了。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代码块抛出的值。

如果你不确定某些代码是否会报错,就可以把它们放在try...catch代码块之中,便于进一步对错误进行处理。

try {
  f();
} catch(e) {
  // 处理错误
}

上面代码中,如果函数f执行报错,就会进行catch代码块,接着对错误进行处理。

catch代码块捕获错误之后,程序不会中断,会按照正常流程继续执行下去。

try {
  throw "出错了";
} catch (e) {
  console.log(111);
}
console.log(222);
// 111
// 222

上面代码中,try代码块抛出的错误,被catch代码块捕获后,程序会继续向下执行。

catch代码块之中,还可以再抛出错误,甚至使用嵌套的try...catch结构。

var n = 100;

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

上面代码中,catch代码之中又抛出了一个错误。

为了捕捉不同类型的错误,catch代码块之中可以加入判断语句。

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);
  }
  // ...
}

上面代码中,catch捕获错误之后,会判断错误类型(EvalError还是RangeError),进行不同的处理。

finally 代码块

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

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

cleansUp()
// 完成清理工作
// Error: 出错了……

上面代码中,由于没有catch语句块,所以错误没有捕获。执行finally代码块以后,程序就中断在错误抛出的地方。

function idle(x) {
  try {
    console.log(x);
    return 'result';
  } finally {
    console.log("FINALLY");
  }
}

idle('hello')
// hello
// FINALLY
// "result"

上面代码说明,try代码块没有发生错误,而且里面还包括return语句,但是finally代码块依然会执行。注意,只有在其执行完毕后,才会显示return语句的值。

下面的例子说明,return语句的执行是排在finally代码之前,只是等finally代码执行完毕后才返回。

var count = 0;
function countUp() {
  try {
    return count;
  } finally {
    count++;
  }
}

countUp()
// 0
count
// 1

上面代码说明,return语句的count的值,是在finally代码块运行之前就获取了。

下面是finally代码块用法的典型场景。

openFile();

try {
  writeFile(Data);
} catch(e) {
  handleError(e);
} finally {
  closeFile();
}

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

下面的例子充分反映了try...catch...finally这三者之间的执行顺序。

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

上面代码中,catch代码块结束执行之前,会先执行finally代码块。

catch代码块之中,触发转入finally代码快的标志,不仅有return语句,还有throw语句。

function f() {
  try {
    throw '出错了!';
  } catch(e) {
    console.log('捕捉到内部错误');
    throw e; // 这句原本会等到finally结束再执行
  } finally {
    return false; // 直接返回
  }
}

try {
  f();
} catch(e) {
  // 此处不会执行
  console.log('caught outer "bogus"');
}

//  捕捉到内部错误

上面代码中,进入catch代码块之后,一遇到throw语句,就会去执行finally代码块,其中有return false语句,因此就直接返回了,不再会回去执行catch代码块剩下的部分了。

NaN

(1)含义

NaN是 JavaScript 的特殊值,表示“非数字”(Not a Number),主要出现在将字符串解析成数字出错的场合。

5 - 'x' // NaN

上面代码运行时,会自动将字符串x转为数值,但是由于x不是数值,所以最后得到结果为NaN,表示它是“非数字”(NaN)。

另外,一些数学函数的运算结果会出现NaN

Math.acos(2) // NaN
Math.log(-1) // NaN
Math.sqrt(-1) // NaN

0除以0也会得到NaN

0 / 0 // NaN

需要注意的是,NaN不是独立的数据类型,而是一个特殊数值,它的数据类型依然属于Number,使用typeof运算符可以看得很清楚。

typeof NaN // 'number'

(2)运算规则

NaN不等于任何值,包括它本身。

NaN === NaN // false

数组的indexOf方法内部使用的是严格相等运算符,所以该方法对NaN不成立。

[NaN].indexOf(NaN) // -1

NaN在布尔运算时被当作false

Boolean(NaN) // false

NaN与任何数(包括它自己)的运算,得到的都是NaN

NaN + 32 // NaN
NaN - 32 // NaN
NaN * 32 // NaN
NaN / 32 // NaN

进制转换

parseInt方法还可以接受第二个参数(2到36之间),表示被解析的值的进制,返回该值对应的十进制数。默认情况下,parseInt的第二个参数为10,即默认是十进制转十进制。

parseInt('1000') // 1000
// 等同于
parseInt('1000', 10) // 1000

下面是转换指定进制的数的例子。

parseInt('1000', 2) // 8
parseInt('1000', 6) // 216
parseInt('1000', 8) // 512

上面代码中,二进制、六进制、八进制的1000,分别等于十进制的8、216和512。这意味着,可以用parseInt方法进行进制的转换。

如果第二个参数不是数值,会被自动转为一个整数。这个整数只有在2到36之间,才能得到有意义的结果,超出这个范围,则返回NaN。如果第二个参数是0undefinednull,则直接忽略。

parseInt('10', 37) // NaN
parseInt('10', 1) // NaN
parseInt('10', 0) // 10
parseInt('10', null) // 10
parseInt('10', undefined) // 10

如果字符串包含对于指定进制无意义的字符,则从最高位开始,只返回可以转换的数值。如果最高位无法转换,则直接返回NaN

parseInt('1546', 2) // 1
parseInt('546', 2) // NaN

上面代码中,对于二进制来说,1是有意义的字符,546都是无意义的字符,所以第一行返回1,第二行返回NaN

前面说过,如果parseInt的第一个参数不是字符串,会被先转为字符串。这会导致一些令人意外的结果。

parseInt(0x11, 36) // 43
parseInt(0x11, 2) // 1

// 等同于
parseInt(String(0x11), 36)
parseInt(String(0x11), 2)

// 等同于
parseInt('17', 36)
parseInt('17', 2)

上面代码中,十六进制的0x11会被先转为十进制的17,再转为字符串。然后,再用36进制或二进制解读字符串17,最后返回结果431

这种处理方式,对于八进制的前缀0,尤其需要注意。

parseInt(011, 2) // NaN

// 等同于
parseInt(String(011), 2)

// 等同于
parseInt(String(9), 2)

上面代码中,第一行的011会被先转为字符串9,因为9不是二进制的有效字符,所以返回NaN。如果直接计算parseInt('011', 2) 011则是会被当作二进制处理,返回3。

JavaScript 不再允许将带有前缀0的数字视为八进制数,而是要求忽略这个0。但是,为了保证兼容性,大部分浏览器并没有部署这一条规定。

对象

概述

生成方法

对象(object)是 JavaScript 语言的核心概念,也是最重要的数据类型。

什么是对象?简单说,对象就是一组“键值对”(key-value)的集合,是一种无序的复合数据集合。

var obj = {
  foo: 'Hello',
  bar: 'World'
};

上面代码中,大括号就定义了一个对象,它被赋值给变量obj,所以变量obj就指向一个对象。该对象内部包含两个键值对(又称为两个“成员”),第一个键值对是foo: 'Hello',其中foo是“键名”(成员的名称),字符串Hello是“键值”(成员的值)。键名与键值之间用冒号分隔。第二个键值对是bar: 'World'bar是键名,World是键值。两个键值对之间用逗号分隔。

键名

对象的所有键名都是字符串(ES6 又引入了 Symbol 值也可以作为键值),所以加不加引号都可以。上面的代码也可以写成下面这样。

var obj = {
  'foo': 'Hello',
  'bar': 'World'
};

如果键名是数值,会被自动转为字符串。

var obj = {
  1: 'a',
  3.2: 'b',
  1e2: true,
  1e-2: true,
  .234: true,
  0xFF: true
};

obj
// Object {
//   1: "a",
//   3.2: "b",
//   100: true,
//   0.01: true,
//   0.234: true,
//   255: true
// }

obj['100'] // true

上面代码中,对象obj的所有键名虽然看上去像数值,实际上都被自动转成了字符串。

如果键名不符合标识名的条件(比如第一个字符为数字,或者含有空格或运算符),且也不是数字,则必须加上引号,否则会报错。

// 报错
var obj = {
  1p: 'Hello World'
};

// 不报错
var obj = {
  '1p': 'Hello World',
  'h w': 'Hello World',
  'p+q': 'Hello World'
};

上面对象的三个键名,都不符合标识名的条件,所以必须加上引号。

对象的每一个键名又称为“属性”(property),它的“键值”可以是任何数据类型。如果一个属性的值为函数,通常把这个属性称为“方法”,它可以像函数那样调用。

var obj = {
  p: function (x) {
    return 2 * x;
  }
};

obj.p(1) // 2

上面代码中,对象obj的属性p,就指向一个函数。

如果属性的值还是一个对象,就形成了链式引用。

var o1 = {};
var o2 = { bar: 'hello' };

o1.foo = o2;
o1.foo.bar // "hello"

上面代码中,对象o1的属性foo指向对象o2,就可以链式引用o2的属性。

对象的属性之间用逗号分隔,最后一个属性后面可以加逗号(trailing comma),也可以不加。

var obj = {
  p: 123,
  m: function () { ... },
}

上面的代码中,m属性后面的那个逗号,有没有都可以。

属性可以动态创建,不必在对象声明时就指定。

var obj = {};
obj.foo = 123;
obj.foo // 123

上面代码中,直接对obj对象的foo属性赋值,结果就在运行时创建了foo属性。

对象的引用

如果不同的变量名指向同一个对象,那么它们都是这个对象的引用,也就是说指向同一个内存地址。修改其中一个变量,会影响到其他所有变量。

var o1 = {};"hello world"
var o2 = o1;

o1.a = 1;
o2.a // 1

o2.b = 2;
o1.b // 2

上面代码中,o1o2指向同一个对象,因此为其中任何一个变量添加属性,另一个变量都可以读写该属性。

此时,如果取消某一个变量对于原对象的引用,不会影响到另一个变量。

var o1 = {};
var o2 = o1;

o1 = 1;
o2 // {}

上面代码中,o1o2指向同一个对象,然后o1的值变为1,这时不会对o2产生影响,o2还是指向原来的那个对象。

但是,这种引用只局限于对象,如果两个变量指向同一个原始类型的值。那么,变量这时都是值的拷贝。

var x = 1;
var y = x;

x = 2;
y // 1

上面的代码中,当x的值发生变化后,y的值并不变,这就表示yx并不是指向同一个内存地址。

表达式还是语句?

对象采用大括号表示,这导致了一个问题:如果行首是一个大括号,它到底是表达式还是语句?

{ foo: 123 }

JavaScript 引擎读到上面这行代码,会发现可能有两种含义。第一种可能是,这是一个表达式,表示一个包含foo属性的对象;第二种可能是,这是一个语句,表示一个代码区块,里面有一个标签foo,指向表达式123

为了避免这种歧义,JavaScript 规定,如果行首是大括号,一律解释为语句(即代码块)。如果要解释为表达式(即对象),必须在大括号前加上圆括号。

({ foo: 123})

这种差异在eval语句(作用是对字符串求值)中反映得最明显。

eval('{foo: 123}') // 123
eval('({foo: 123})') // {foo: 123}

上面代码中,如果没有圆括号,eval将其理解为一个代码块;加上圆括号以后,就理解成一个对象。

属性的操作

读取属性

读取对象的属性,有两种方法,一种是使用点运算符,还有一种是使用方括号运算符。

var obj = {
  p: 'Hello World'
};

obj.p // "Hello World"
obj['p'] // "Hello World"

上面代码分别采用点运算符和方括号运算符,读取属性p

请注意,如果使用方括号运算符,键名必须放在引号里面,否则会被当作变量处理。

var foo = 'bar';

var obj = {
  foo: 1,
  bar: 2
};

obj.foo  // 1
obj[foo]  // 2

上面代码中,引用对象objfoo属性时,如果使用点运算符,foo就是字符串;如果使用方括号运算符,但是不使用引号,那么foo就是一个变量,指向字符串bar

方括号运算符内部还可以使用表达式。

obj['hello' + ' world']
obj[3 + 3]

数字键可以不加引号,因为会自动转成字符串。

var obj = {
  0.7: 'Hello World'
};

obj['0.7'] // "Hello World"
obj[0.7] // "Hello World"

上面代码中,对象obj的数字键0.7,加不加引号都可以,因为会被自动转为字符串。

注意,数值键名不能使用点运算符(因为会被当成小数点),只能使用方括号运算符。

var obj = {
  123: 'hello world'
};

obj.123 // 报错
obj[123] // "hello world"

上面代码的第一个表达式,对数值键名123使用点运算符,结果报错。第二个表达式使用方括号运算符,结果就是正确的。

属性的赋值

点运算符和方括号运算符,不仅可以用来读取值,还可以用来赋值。

var obj = {};

obj.foo = 'Hello';
obj['bar'] = 'World';

上面代码中,分别使用点运算符和方括号运算符,对属性赋值。

JavaScript 允许属性的“后绑定”,也就是说,你可以在任意时刻新增属性,没必要在定义对象的时候,就定义好属性。

var obj = { p: 1 };

// 等价于

var obj = {};
obj.p = 1;

查看所有属性

查看一个对象本身的所有属性,可以使用Object.keys方法。

var obj = {
  key1: 1,
  key2: 2
};

Object.keys(obj);
// ['key1', 'key2']

delete 命令

delete命令用于删除对象的属性,删除成功后返回true

var obj = { p: 1 };
Object.keys(obj) // ["p"]

delete obj.p // true
obj.p // undefined
Object.keys(obj) // []

上面代码中,delete命令删除对象objp属性。删除后,再读取p属性就会返回undefined,而且Object.keys方法的返回值也不再包括该属性。

注意,删除一个不存在的属性,delete不报错,而且返回true

var obj = {};
delete obj.p // true

上面代码中,对象obj并没有p属性,但是delete命令照样返回true。因此,不能根据delete命令的结果,认定某个属性是存在的。

只有一种情况,delete命令会返回false,那就是该属性存在,且不得删除。

var obj = Object.defineProperty({}, 'p', {
  value: 123,
  configurable: false
});

obj.p // 123
delete obj.p // false

上面代码之中,对象objp属性是不能删除的,所以delete命令返回false(关于Object.defineProperty方法的介绍,请看《标准库》的 Object 对象一章)。

另外,需要注意的是,delete命令只能删除对象本身的属性,无法删除继承的属性(关于继承参见《面向对象编程》章节)。

var obj = {};
delete obj.toString // true
obj.toString // function toString() { [native code] }

上面代码中,toString是对象obj继承的属性,虽然delete命令返回true,但该属性并没有被删除,依然存在。这个例子还说明,即使delete返回true,该属性依然可能读取到值。

in 运算符

in运算符用于检查对象是否包含某个属性(注意,检查的是键名,不是键值),如果包含就返回true,否则返回false

var obj = { p: 1 };
'p' in obj // true

in运算符的一个问题是,它不能识别哪些属性是对象自身的,哪些属性是继承的。

var obj = {};
'toString' in obj // true

上面代码中,toString方法不是对象obj自身的属性,而是继承的属性。但是,in运算符不能识别,对继承的属性也返回true

for…in 循环

for...in循环用来遍历一个对象的全部属性。

var obj = {a: 1, b: 2, c: 3};

for (var i in obj) {
  console.log(obj[i]);
}
// 1
// 2
// 3

下面是一个使用for...in循环,提取对象属性名的例子。

var obj = {
  x: 1,
  y: 2
};
var props = [];
var i = 0;

for (var p in obj) {
  props[i++] = p
}

props // ['x', 'y']

for...in循环有两个使用注意点。

  • 它遍历的是对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性。
  • 它不仅遍历对象自身的属性,还遍历继承的属性。

举例来说,对象都继承了toString属性,但是for...in循环不会遍历到这个属性。

var obj = {};
// toString 属性是存在的
obj.toString // toString() { [native code] }

for (var p in obj) {
  console.log(p);
} // 没有任何输出

上面代码中,对象obj继承了toString属性,该属性不会被for...in循环遍历到,因为它默认是“不可遍历”的。关于对象属性的可遍历性,参见《标准库》章节中 Object 一章的介绍。

如果继承的属性是可遍历的,那么就会被for...in循环遍历到。但是,一般情况下,都是只想遍历对象自身的属性,所以使用for...in的时候,应该结合使用hasOwnProperty方法,在循环内部判断一下,某个属性是否为对象自身的属性。

var person = { name: '老张' };

for (var key in person) {
  if (person.hasOwnProperty(key)) {
    console.log(key);
  }
}
// name

with 语句

with语句的格式如下:

with (对象) {
  语句;
}

它的作用是操作同一个对象的多个属性时,提供一些书写的方便。

// 例一
var obj = {
  p1: 1,
  p2: 2,
};
with (obj) {
  p1 = 4;
  p2 = 5;
}
// 等同于
obj.p1 = 4;
obj.p2 = 5;

// 例二
with (document.links[0]){
  console.log(href);
  console.log(title);
  console.log(style);
}
// 等同于
console.log(document.links[0].href);
console.log(document.links[0].title);
console.log(document.links[0].style);

注意,如果with区块内部有变量的赋值操作,必须是当前对象已经存在的属性,否则会创造一个当前作用域的全局变量。

var obj = {};
with (obj) {
  p1 = 4;
  p2 = 5;
}

obj.p1 // undefined
p1 // 4

上面代码中,对象obj并没有p1属性,对p1赋值等于创造了一个全局变量p1。正确的写法应该是,先定义对象obj的属性p1,然后在with区块内操作它。

这是因为with区块没有改变作用域,它的内部依然是当前作用域。这造成了with语句的一个很大的弊病,就是绑定对象不明确。

with (obj) {
  console.log(x);
}

单纯从上面的代码块,根本无法判断x到底是全局变量,还是对象obj的一个属性。这非常不利于代码的除错和模块化,编译器也无法对这段代码进行优化,只能留到运行时判断,这就拖慢了运行速度。因此,建议不要使用with语句,可以考虑用一个临时变量代替with

with(obj1.obj2.obj3) {
  console.log(p1 + p2);
}

// 可以写成
var temp = obj1.obj2.obj3;
console.log(temp.p1 + temp.p2);

参考链接











猜你喜欢

转载自blog.csdn.net/aerchi/article/details/80050354