第三章 基础
第三章 基础
var
未加var关键字的变量声明会被看作全局变量,严格模式下会抛出异常。
var声明的变量会被提升至其作用域顶部。
let
let声明是块级作用域,不允许重复声明,但嵌套可以:
let a = 66;
if (true) {
let a = 666;
console.log(a); //666
}
let声明的变量不会被提升。
let声明的全局变量不会被添加到window对象。
typeof运算符
let name = 'Ciri';
let age = 18;
console.log(typeof name !== 'undefined'); //true
if (typeof name !== 'undefined') {
let name = 'Geralt'; //会被限制在块内
}
try {
let age = 22;
} catch (error) {
let age = 20;
}
console.log(age); //18
循环里的let
for (var i = 0; i < 5; ++i) {
setTimeout(() => console.log(i), 0);
//这段代码会输出数字5五次。这是因为setTimeout函数创建了一个函数(一个闭包),它将在当前执行栈清空后的某个时刻执行。由于var声明的变量是函数作用域的,这意味着i变量是全局的,而不是循环中每次迭代的局部变量。循环结束时,i的值是5,因此每个setTimeout函数引用的都是同一个全局i,并且它的值是循环结束时的值。
}
for (let j = 0; j < 5; ++j) {
setTimeout(() => console.log(j), 0)
// 0,1,2,3,4 let每次循环都声明一个新的迭代器变量,每个setTimeout引用单独的迭代器变量。
}
这种每次迭代都声明变量的行为在for...in和for...of中也是一样的。
for (const key in { a: 1, b: 666 }) {
console.log(key);//a,b 键
}
for (const value of [1, 2, 3, 4, 5]) {
console.log(value);//1,2,3,4,5 值
}
const
const约束的对象,可以修改其属性:
for(const key in {a:1,b:666}){
console.log(key);//a,b
}
for(const value of [1,2,3,4,5]){
console.log(value);//1,2,3,4,5
}
数据类型
最新的ECMAScript标准定义了9种数据类型:
6种原始类型
- Boolean
- Bigint
- String
- Symbol
- undefined
- Number
Bigint是通过给整数后面添加n或通过构造函数创建的:
const x = 2 ** 53;
console.log(x); //9007199254740992
const y = x + 1;
console.log(y); //9007199254740992
const z = 2n ** 53n + 1n;
console.log(z); //9007199254740993n
2种结构类型
- Object ,特殊的非数据结构类型,但是对于构造的对象实例可用作数据类型,如:new Object, new Array, new Map, new Set, new WeakMap, new WeakSet, new Date等几乎所有使用new关键字创建的对象。
- Function 非数据结构。
null
结构根原始类型(Structural Root Primitive):null,特殊的原始类型,其值有特殊的用法:如果未继承对象,则显示null。
typeof null 返回"object",null为空对象的引用。
console.log(typeof null);//object
console.log(undefined == null);//true
console.log(undefined === null);//false
var a = {};
console.log(a === null);//false
console.log(a == null);//false
var c = function(){};
console.log(typeof c);//function
undefined
let name = 'Ciri';
//let age = 18;
console.log(name);//Ciri
console.log(age);//抛出异常
console.log(typeof name);//string
console.log(typeof age);//undefined
Number(value) 进行更严谨的解析,只要参数带有无效字符就会被转换为NaN 。
parseInt(string,radix)方法解析一个字符串,并返回一个整数;其第二个参数可选,范围为[2,36]。
如果输入的 string以 "0x"或 "0X"开头,那么radix被假定为 16 ,字符串的其余部分被当做十六进制数去解析。
如果输入的 string以 "0"开头,radix被假定为 8 或 10 。具体选择哪一个radix取决于实现。ES5使用 10 ,但不是所有的浏览器都支持。因此,在使用parseInt时,一定要指定一个radix。
如果输入的 string 以任何其他值开头,radix是 10 。
parseFloat是个全局函数,不属于任何对象 。
如果parseFloat在解析过程中遇到了正号(+)、负号(-)、数字(0-9)、小数点(.)、或者科学记数法中的指数(e或E)以外的字符,则它会忽略该字符以及之后的所有字符,返回当前已经解析到的浮点数。
第二个小数点的出现也会使解析停止(在这之前的字符都会被解析)。
如果参数字符串的第一个字符不能被解析成为数字,则parseFloat返回NaN。
示例:
var a = NaN;
console.log(a != a);//true
console.log(Number(undefined));//NaN
console.log(Number(''));//
console.log(parseInt('')); // NaN
console.log(parseInt('0123cctv')); //
console.log(parseInt('0xF')); //
console.log(parseFloat('0123cctv')); //
console.log(parseFloat('0xF')); //
toString()用于数字时,可选择进制位数作为参数:
let num = 10;
console.log(num.toString()); // "10"
console.log(num.toString(2)); // "1010"
console.log(num.toString(8)); // "12"
console.log(num.toString(10)); // "10"
console.log(num.toString(16)); // "a"
模板字符串:
let name = `Ciri`;
let age = 18 ;
let mystring = 'I\'m ' + name + ' and ' + (age + 1 ) + ' years old';
let herstring =`I'm ${name} and ${age} years old`;
//模板字符串不必使用反斜杠转义单双引号
let mString = `南风知我意
吹梦到西洲`;
console.log(mystring);//I'm Ciri and 19 years old
console.log(herstring);//I'm Ciri and 18 years old
console.log(mString);
//南风知我意
//
// 吹梦到西洲
模板字符串支持定义标签函数(tag functions),标签函数支持自定义插入行为。模板被插入符拆分为单独的片段后传入标签函数,随后进行计算。
标签函数定义为常规函数,通过放在模板字符串前面使用。其第一个参数为生字符串(raw strings)数组,其余参数是插入表达式计算的结果。返回值是从模板字符串计算得到的字符串。如下所示:
let name = `Ciri`;
let age = 18;
function greet(a, b, c) {
console.log(a);
console.log(b);
console.log(c);
}
greet `I'm${name},${age} years old.`;
//["I'm", ",", " years old."]
//Ciri
//18
因为表达式参数的值是可变的,使用延展操作符'...'组合它们会是不错的选择:
let name = `Ciri`;
let age = 18;
function zipTag(a, ...b) {
return a[0] + b.map((e, i) => `${e}${a[i+1]}`).join('');
//map返回数组,为b中每个元素e执行一次箭头函数
}
let untaggedname = `I'm ${name},${age} years old.`;
let taggedname = zipTag `I'm ${name},${age} years old.`;
console.log(untaggedname); //I'm Ciri,18 years old.
console.log(taggedname); //I'm Ciri,18 years old.
string.raw:
console.log(`\u00A9`); //©
console.log(String.raw `\u00A9`); //\u00A
console.log(String.raw`first line\nsecond line`);
// "first line\nsecond line"
function printRaw(a) {
//console.log(a);
console.log('实际字符');
for (const s of a) {
console.log(s);
}
console.log('原始字符');
//a.raw为数组a的一个属性,值为["\u00A9", "\n"]
for (const r of a.raw) {
console.log(r);
}
}
printRaw `\u00A9${'Ciri'}\n`;
//实际字符
//©
//(空行)
//原始字符
//\u00A
//\n
Symbol类型
ES6中的新增了Symbol数据类型,Symbol是原始值类型,它的实例是唯一且不可变的。
尽管此类型与私有属性有些相似,但symbol并不是为了私有属性这种行为而提供的。symbol的目的是用作独一无二的标记,用于特殊属性的键。
symbol基础用法
Symbol使用Symbol()函数实例化(不能使用new实例化),类型是symbol:
let sym = Symbol();
console.log(typeof sym); // symbol
let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();
let fooSymbol = Symbol('foo');
let otherFooSymbol = Symbol('foo');
console.log(genericSymbol == otherGenericSymbol); // false
console.log(fooSymbol == otherFooSymbol); // false
//let mySymbol = new Symbol(); // TypeError: Symbol is not a constructor.
let mySymbol = Symbol();
let myWrappedSymbol = Object(mySymbol);
console.log(typeof myWrappedSymbol); // "object"
使用全局Symbol注册表
在运行时的不同地方希望共享和重用symbol实例的情况下,可以在全局symbol注册表中创建和重用symbol,这可通过Symbol.for()来完成:
let fooGlobalSymbol = Symbol.for('foo');
console.log(typeof fooGlobalSymbol); // symbol
Symbol.for()对每个字符串键(string key)的操作是幂等的,使用给定的字符串第一次调用该方法时,将检查全局运行时注册表,若发现无相应symbol存在,就实例化一个新Symbol,并添加到注册表;额外调用相同字符串键时将检查全局运行时注册表,若发现存在那个字符串键的symbol,就返回此实例:
let fooGlobalSymbol = Symbol.for('foo'); // 创建新symbol
let otherFooGlobalSymbol = Symbol.for('foo'); // 重用存在的symbol
console.log(fooGlobalSymbol === otherFooGlobalSymbol); // true
全局注册表中定义的symbol与使用Symbol()创建的symbol完全不同,即使它们共享同一个描述:
let localSymbol = Symbol('foo');
let globalSymbol = Symbol.for('foo');
console.log(localSymbol === globalSymbol); // false
全局注册表需要字符串键,因此,提供给Symbol.for()的参数的任何内容都将转换为字符串。此外,用于注册表的键也将用作symbol说明:
let emptyGlobalSymbol = Symbol.for();
console.log(emptyGlobalSymbol); // Symbol(undefined)
可以使用Symbol.keyFor()来检查全局注册表,该方法接受一个symbol并返回该全局symbol的全局字符串键;如果该symbol不是全局symbol,则为undefined。
// 创建全局symbol
let s = Symbol.for('foo');
console.log(Symbol.keyFor(s)); // foo
// 创建普通symbol
let s2 = Symbol('bar');
console.log(Symbol.keyFor(s2)); // undefined
//Symbol.keyFor(123); // TypeError: 123 is not a symbol
将Symbol用作属性
通常在可以使用字符串或数字属性的地方,也可以使用Symbol。这包括对象字面量属性、Object.defineProperty(和Object.defineProperties()中。对象字面量只能在计算属性语法内(加‘[]’)将symbol用作属性:
let s1 = Symbol('foo'),
s2 = Symbol('bar'),
s3 = Symbol('baz'),
s4 = Symbol('qux');
let o = {
[s1]: 'foo val'
};
// 这样也有效: o[s1] = 'foo val';
console.log(o); // {Symbol{foo}: foo val}
Object.defineProperty(o, s2, {
value: 'bar val'
});
console.log(o); // {Symbol{foo}: foo val, Symbol(bar): bar val}
Object.defineProperties(o, {
[s3]: {
value: 'baz val'
},
[s4]: {
value: 'qux val'
}
});
console.log(o);
// {Symbol{foo}: foo val, Symbol(bar): bar val,
// Symbol{baz}: baz val, Symbol(qux): qux val}
Object.getOwnPropertySymbols()返回对象实例上仅是Symbol属性的数组。Object.getOwnPropertyDescriptors()将返回一个包含普通属性或Symbol属性的描述符对象。Reflect.ownKeys()返回两种类型的键:
let s1 = Symbol('foo'),
s2 = Symbol('bar');
let o = {
[s1]: 'foo val',
[s2]: 'bar val',
baz: 'baz val',
qux: 'qux val'
};
console.log(Object.getOwnPropertySymbols(o));
// [Symbol(foo), Symbol(bar)]
console.log(Object.getOwnPropertyNames(o));
// ["baz", "qux"]
console.log(Object.getOwnPropertyDescriptors(o));
// {baz: {...}, qux: {...}, Symbol(foo): {...}, Symbol(bar): {...}}
console.log(Reflect.ownKeys(o));
// ["baz", "qux", Symbol(foo), Symbol(bar)]
因为symbol属性算作内存中的一个引用,symbol直接创建并使用并不会丢失。然而,不显式保持symbol
属性的引用的话需要在对象中遍历所有symbol属性才能找到对应属性键:
let o = {
[Symbol('foo')]: 'foo val',
[Symbol('bar')]: 'bar val'
};
console.log(o);
// {Symbol(foo): "foo val", Symbol(bar): "bar val"}
let barSymbol = Object.getOwnPropertySymbols(o).find((symbol) =>
symbol.toString().match(/bar/));
console.log(barSymbol); //Symbol(bar)
广为人知的Symbol
ES6引入了一系列众所周知的symbol,这些symbol将在整个语言中使用,以公开内部语言的行为,以便直接访问、覆写或模仿。这些众所周知的symbol在symbol工厂函数中作为字符串属性存在。
这些众所周知的symbol的主要用途之一是对其进行重新定义,以改变原生语言结构的行为。例如,由于已知for...of循环将在任何可迭代对象上使用Symbol.iterator属性,因此可以在自定义对象中提供Symbol.iterator的本地实现,以便控制该对象在for...of上的行为。
这些众所周知的symbol没有什么特别的,它们是Symbol上的常规字符串属性(以全局的Symbol实例为键)。每个定义良好的symbol属性都是不可写、不可枚举和不可配置的。
注意:在有关ECMAScript规范的讨论中,经常会看到这些symbol以其规范名称指代,并以@@开头。例如,@@iterator指代Symbol.iterator。
Symbol.asyncIterator
根据ECMAScript规范(Symbol.asyncIterator属于ES2018规范),此Symbol属性是"一个返回对象默认AsyncIterator的方法,由for-await-of语句的语义调用"。它用于识别实现异步迭代器API的函数。
语言结构如for-await-of用这个函数执行异步迭代,它们将调用键为Symbol.asyncIterator的函数并期待返回一个实现迭代器API的对象。大多数情况下,该对象是AsyncGenerator,如下所示:
class Foo {
async *[Symbol.asyncIterator]() {}
}
let f = new Foo();
console.log(f[Symbol.asyncIterator]());
// AsyncGenerator {<suspended>}
特别地,Symbol.asyncIterator产生的对象应通过调用其next()方法不断地产生Promise实例,这可通过显式定义next()方法实现的或通过异步生成器函数隐式地实现:
class Emitter {
constructor(max) {
this.max = max;
this.asyncIdx = 0;
}
// 异步生成器函数
async *[Symbol.asyncIterator]() {
while (this.asyncIdx < this.max) {
yield new Promise((resolve) => resolve(this.asyncIdx++));
}
}
}
let asyncEmitter = new Emitter(6);
let asyncGen = asyncEmitter[Symbol.asyncIterator]();
console.log(asyncGen); //AsyncGenerator {<suspended>}
async function asyncCount() {
for await (const x of asyncEmitter) {
console.log(x);
}
}
asyncCount();//0,1,2,3,4,5
Symbol.hasInstance
根据ECMAScript规范,此Symbol属性是"一个方法,决定一个构造函数对象是否能够识别一个对象是该构造函数的实例,由instanceof运算符的语义调用"。
function Foo() {}
let f = new Foo();
console.log(f instanceof Foo); // true
class Bar {}
let b = new Bar();
console.log(b instanceof Bar); // true
在ES6中,instanceof运算符使用Symbol.hasInstance函数计算关系:
function Foo() {}
let f = new Foo();
console.log(Foo[Symbol.hasInstance](f)); // true
class Bar {}
let b = new Bar();
console.log(Bar[Symbol.hasInstance](b)); // true
此属性是在Function原型上定义的,因此默认情况下,该属性可用于所有函数和类。因为instanceof运算符将像查找任何其他属性一样在原型链上查找属性定义,所以可以在继承类上通过静态方法重新定义该函数:
class Bar {}
class Baz extends Bar {
static[Symbol.hasInstance]() {
return false;
}
}
let b = new Baz();
console.log(Bar[Symbol.hasInstance](b)); // true
console.log(b instanceof Bar); // true
console.log(Baz[Symbol.hasInstance](b)); // false
console.log(b instanceof Baz); // false
Symbol.isConcatSpreadable
根据ECMAScript规范,此Symbol属性是"一个布尔值属性,如果为true的话,意味着一个对象可通过调用Array.prototype.concat()方法将数组元素展平"。ES6中的Array.prototype.concat()方法将根据传入的对象类型选择如何将类数组对象合并到数组实例。 Symbol .isConcatSpread的值可以覆写此行为。
默认情况下,数组对象可被展平到现有数组中。Symbol.isConcatSpreadable值为false或传入错误值会将整个对象添加到数组。默认情况下,类数组对象可被添加到数组。Symbol.isConcatSpreadable值为true或传入正
确值会将类数组对象展平到数组实例中。当Symbol.isConcatSpread设置为true时,其他非类数组对象将被忽略:
let initial = ['foo'];
let array = ['bar'];
console.log(array[Symbol.isConcatSpreadable]); // undefined
console.log(initial.concat(array)); // ['foo', 'bar']
array[Symbol.isConcatSpreadable] = false;
console.log(initial.concat(array)); // ['foo', Array(1)]
let arrayLikeObject = {
length: 1,
0: 'baz'
};
console.log(arrayLikeObject[Symbol.isConcatSpreadable]); // undefined
console.log(initial.concat(arrayLikeObject)); // ['foo', {...}]
arrayLikeObject[Symbol.isConcatSpreadable] = true;
console.log(initial.concat(arrayLikeObject)); // ['foo', 'baz']
let otherObject = new Set().add('qux');
console.log(otherObject[Symbol.isConcatSpreadable]); // undefined
console.log(initial.concat(otherObject)); // ['foo', Set(1)]
otherObject[Symbol.isConcatSpreadable] = true;
console.log(initial.concat(otherObject)); // ['foo']
Symbol.iterator
根据ECMAScript规范,此Symbol属性是"一个返回对象默认迭代器的方法,由for...of语句的语义调用"。用于识别实现迭代器API的函数。
语言结构(如for...of循环)利用此函数执行迭代。它们将调用键为Symbol.iterator的函数,并期望它返回一个实现迭代器API的对象。在大多数情况下,返回的是Generator对象:
class Foo {
*[Symbol.iterator]() {}
}
let f = new Foo();
console.log(f[Symbol.iterator]());
// Generator {<suspended>}
技术上讲,由Symbol.iterator函数生成的对象应通过调用其next()方法连续生成值,这可通过显式定义next()方法实现或通过生成器函数隐式地实现:
class Emitter {
constructor(max) {
this.max = max;
this.idx = 0;
}
// 生成器函数
*[Symbol.iterator]() {
while (this.idx < this.max) {
yield this.idx++;
}
}
}
let e = new Emitter(6);
console.log(...e); //0 1 2 3 4 5
Symbol.match
根据ECMAScript规范,此Symbol属性是"一个将正则表达式与字符串匹配的正则表达式方法。由String.prototype.match()方法调用"。 String.prototype.match()方法将使用键为Symbol.match的函数计算表达式 。默认情况下,正则表达式原型有定义此方法,所有正则表达式实例都是match()方法的有效参数:
console.log(RegExp.prototype[Symbol.match]);
// ƒ [Symbol.match]() { [native code] }
console.log('foobar'.match(/bar/));
// ["bar", index: 3, input: "foobar", groups: undefined]
向此方法提供除正则表达式以外的内容会隐式地使用 new RegExp(obj) 将其转换为一个RegExp。如果希望规避此行为并让该方法直接使用该参数,可以传入非正则表达式参数给match()方法。可通过定义Symbol.match函数来取代原本由正则表达式表现出来的行为(参数target为调用match方法的字符串):
class FooMatcher {
static[Symbol.match](target) {
return target.includes('foo');
}
}
// 类静态方法直接通过类名调用
console.log('foobar'.match(FooMatcher)); // true
console.log('barbaz'.match(FooMatcher)); // false
class StringMatcher {
constructor(str) {
this.str = str;
}
[Symbol.match](target) {
return target.includes(this.str);
}
}
// 类成员方法通过实例调用
console.log('foobar'.match(new StringMatcher('foo'))); // true
console.log('barbaz'.match(new StringMatcher('qux'))); // false
Symbol.replace
根据ECMAScript规范,此Symbol属性是"一个正则表达式方法,用于替换字符串中匹配的子字符串。由String.prototype.replace()方法调用"。 String.prototype.replace()方法将使用键为Symbol.replace的函数计算表达式 。正则表达式原型默认拥有此函数定义,因此所有正则表达式实例都是replace()方法的有效参数:
console.log(RegExp.prototype[Symbol.replace]);
// ƒ [Symbol.replace]() { [native code] }
console.log('foobarbaz'.replace(/bar/, 'qux'));
// 'fooquxbaz'
向此方法提供除正则表达式以外的内容将导致其转换为RegExp对象。如果希望规避此行为并让该方法直接使用该参数,可以传入非正则表达式参数给replace()方法。可通过定义Symbol.match函数来取代原本由正则表达式表现出来的行为:
class FooReplacer {
static[Symbol.replace](target, replacement) {
return target.split('foo').join(replacement);
}
}
console.log('barfoobaz'.replace(FooReplacer, 'qux'));
// "barquxbaz"
class StringReplacer {
constructor(str) {
this.str = str;
}
[Symbol.replace](target, replacement) {
return target.split(this.str).join(replacement);
}
}
console.log('barfoobaz'.replace(new StringReplacer('foo'), 'qux'));
// "barquxbaz"
Symbol.search
根据ECMAScript规范,此Symbol属性是"一个正则表达式方法,它返回字符串中与正则表达式匹配的索引。由String .prototype.search()方法调用"。 String.prototype.search()使用键为Symbol.search的方法计算表达式 。正则表达式原型默认拥有此函数定义,因此所有正则表达式实例都是search()方法的有效参数:
向此方法提供除正则表达式以外的内容将导致其转换为RegExp对象。如果希望规避此行为并让该方法直接使用该参数,可以传入非正则表达式参数给search()方法,可通过定义Symbol.search函数来取代原本由正则表达式表现出来的行为:
class FooSearcher {
static[Symbol.search](target) {
return target.indexOf('foo');
}
}
console.log('foobar'.search(FooSearcher)); // 0
console.log('barfoo'.search(FooSearcher)); // 3
console.log('barbaz'.search(FooSearcher)); // -1
class StringSearcher {
constructor(str) {
this.str = str;
}
[Symbol.search](target) {
return target.indexOf(this.str);
}
}
console.log('foobar'.search(new StringSearcher('foo'))); // 0
console.log('barfoo'.search(new StringSearcher('foo'))); // 3
console.log('barbaz'.search(new StringSearcher('qux'))); // -1
Symbol.species
根据ECMAScript规范,此Symbol属性是"一个函数值属性,它是用于创建派生对象的构造函数"。使用Symbol.species定义静态getter方法,可以覆写新创建实例上的原型定义:
class Bar extends Array {}
class Baz extends Array {
static get[Symbol.species]() {
return Array;
}
}
let bar = new Bar();
console.log(bar instanceof Array); // true
console.log(bar instanceof Bar); // true
bar = bar.concat('bar');
console.log(bar instanceof Array); // true
console.log(bar instanceof Bar); // true
let baz = new Baz();
console.log(baz); //Baz [] 原型为Array,构造函数为Baz
console.log(baz instanceof Array); // true
console.log(baz instanceof Baz); // true
baz = baz.concat('baz'); //返回新的Array实例
console.log(baz); //["baz"] 原型为覆写的Array
console.log(baz instanceof Array); // true
console.log(baz instanceof Baz); // false
Symbol.split
根据ECMAScript规范,此Symbol属性是"一个正则表达式方法,该方法在与正则表达式匹配的索引处拆分字符串。由String .prototype.split()方法调用"。 String.prototype.split()方法将使用键为Symbol.split的函数计算表达式 。正则表达式原型默认拥有此函数定义,因此所有正则表达式实例都是split()方法的有效参数:
console.log(RegExp.prototype[Symbol.split]);
// ƒ [Symbol.split]() { [native code] }
console.log('foobarbaz'.split(/bar/));
// ['foo', 'baz']
向此方法提供除正则表达式以外的内容将导致其转换为RegExp对象。如果希望规避此行为并让该方法直接使用该参数,可以传入非正则表达式参数给split()方法,可通过定义Symbol.split函数来取代原本由正则表达式表现出来的行为:
class FooSplitter {
static[Symbol.split](target) {
return target.split('foo');
}
}
console.log('barfoobaz'.split(FooSplitter));
// ["bar", "baz"]
class StringSplitter {
constructor(str) {
this.str = str;
}
[Symbol.split](target) {
return target.split(this.str);
}
}
console.log('barfoobaz'.split(new StringSplitter('foo')));
// ["bar", "baz"]
Symbol.toPrimitive
根据ECMAScript规范,此Symbol属性是"一种将对象转换为相应原始值的方法。由ToPrimitive抽象操作调用"。有许多内置操作会尝试将对象强制转换为原始值:字符串,数字或其他原始类型。对于自定义对象实例,可以通过在实例的Symbol.toPrimitive属性上定义一个函数来改变此行为。
基于提供给自定义函数的字符串参数(字符串,数字或默认值),可以控制返回的原始类型:
class Foo {}
let foo = new Foo();
console.log(3 + foo); // "3[object Object]"
console.log(3 - foo); // NaN
console.log(String(foo)); // "[object Object]"
class Bar {
constructor() {
this[Symbol.toPrimitive] = function(hint) {
switch (hint) {
case 'number':
return 3;
case 'string':
return 'string bar';
case 'default':
default:
return 'default bar';
}
}
}
}
let bar = new Bar();
console.log(3 + bar); // "3default bar"
console.log(3 - bar); // 0
console.log(String(bar)); // "string bar"
Symbol.toStringTag
根据ECMAScript规范,此Symbol属性是"一个字符串值属性,用于创建对象的默认字符串描述。通过内置方法Object.prototype.toString()访问"。
通过toString()方法进行对象识别将获取Symbol.toStringTag指定的实例标识符,默认为Object。内置类型已经指定了此值,但是自定义类实例需要显式定义:
let s = new Set();
console.log(s); // Set(0) {}
console.log(s.toString()); // [object Set]
console.log(s[Symbol.toStringTag]); // Set
class Foo {}
let foo = new Foo();
console.log(foo); // Foo {}
console.log(foo.toString()); // [object Object]
console.log(foo[Symbol.toStringTag]); // undefined
class Bar {
constructor() {
this[Symbol.toStringTag] = 'Bar';
}
}
let bar = new Bar();
console.log(bar); // Bar {Symbol(Symbol.toStringTag): "Bar"}
console.log(bar.toString()); // [object Bar]
console.log(bar[Symbol.toStringTag]); // Bar
Symbol.unscopables
根据ECMAScript规范,此Symbol属性是"一个对象值属性,其自己的属性和继承的属性名称是从关联对象的with环境绑定中排除的属性名称"。with相关,不推荐使用。
Object类型
每个Object实例有如下属性和方法:
constructor 用于创建对象的函数。
hasOwnProperty(propertyName) 指示给定的属性是否在对象实例上(而不是在原型上),属性名必须是
字符串形式如:o.hasOwnProperty("name")。
isPrototypeof(object) 决定该对象是否是object的原型。
propertyIsEnumerable(propertyName) 指示给定的属性是否可用for...in语句枚举。属性名必须是字符
串形式。
toLocaleString() 返回适合对象本地执行环境的字符串表示形式。
toString() 返回对象的字符串表示形式。
valueOf() 返回与对象相等的字符串、数字或布尔值。通常和toString()的值相同。
注意:从技术上讲,ECMA-262中对象的行为不必一定适用于JavaScript中的其他对象。浏览器环境中存在的对象(例如,浏览器对象模型(BOM)和文档对象模型(DOM)中的对象)被视为宿主对象,因为它们是由宿主提供和定义的。宿主对象不受ECMA-262的管辖,因此,可能会也可能不会直接从Object继承。
操作符
逻辑操作符
逻辑非行为如下:
若操作数是对象,则返回false。
若操作数是空字符串,则返回true。
若操作数是非空字符串,则返回false。
若操作数是数字 0 ,则返回true。
若操作数是非零数字(包括Infinity),则返回false。
若操作数是null、NaN、undefined,则返回true。
如下所示:
let o = {};
console.log(!o); //false
console.log(!""); // true
console.log(!"Ciri"); // false
console.log(!0); // true
console.log(!false); // true
console.log(!12345); // false
console.log(!Infinity); //false
console.log(!NaN); // true
console.log(!null); //true
console.log(!undefined); //true
使用两个NOT操作符可以模拟Boolean()转换:
console.log(!!"blue"); // true
console.log(!!0); // false
console.log(!!NaN); // false
console.log(!!""); // false
console.log(!!12345); // true
逻辑AND可以与任何类型的操作数一起使用,而不仅仅是布尔值。当两个操作数都不是原始布尔值时,逻辑AND并不总是返回布尔值;它执行以下操作之一:
如果第一个操作数是对象,则返回第二个操作数。
如果第二个操作数是对象,仅当第一个操作数计算为true时才返回该对象。
如果两个操作数都是对象,则返回第二个操作数。
如果两个操作数都是null,则返回null。
如果两个操作数都是NaN,则返回NaN。
如果两个操作数都是undefined,则返回undefined。
如下所示:
let name = 'Ciri';
let age = 18;
console.log(name && age); //18 若第一个是object,则返回第二个操作数
console.log(age && name); //Ciri
console.log(null && null); //null
console.log(undefined && undefined); //undefined
console.log(NaN && NaN); //NaN
console.log(false && undefinedvar); //短路,不报错
逻辑或OR,它执行以下操作之一:
如果第一个操作数是对象,则返回第一个操作数。
如果第一个操作数计算为false,则返回第二个操作数。
如果两个操作数都是对象,则返回第一个操作数。
如果两个操作数都是null,则返回null。
如果两个操作数都是NaN,则返回NaN。
如果两个操作数都是undefined,则返回undefined。
let name = 'Ciri';
let age = 18;
console.log(name || age); //Ciri 第一个非Boolean对象,返回第一个
console.log(null || name); //Ciri
console.log(null || null); //null
console.log(undefined || undefined); //undefined
console.log(NaN || NaN); //NaN
console.log(true || undefinedvar); //true 短路,不报错
相等"=="和不等"!="运算符会执行操作数转换,如下是基本规则:
如果一个操作数是布尔值,则检查相等前将其转换为数字:false为 0 ,true为 1 。
如果一个操作数是字符串另一个操作数是数字,则检查相等前尝试将字符串转换成数字。
如果一个操作数是对象而另一个不是,则调用对象的valueOf()方法获取一个原始值用于比较(根据前面的规则)。
还有如下规则:
值null和undefined是相等的,且二者不做任何转换。
如果两个操作数都是NaN,则相等运算符返回false,不等运算符返回true。
如果两个操作数指向同一个对象,则返回true,否则返回false。
三等运算符"==="不转换操作数:
console.log(666 == '666');//true
console.log(666 === '666');//false
console.log(null == undefined);//true
console.log(null === undefined);//false
逗号运算符,总是返回最右操作数:
let a = ( 1 , 2 , 3 , 4 , 5 , 6 , 0 , 9 , 8 , 666 );
console.log(a);//666
for...in 和 for...of区别:前者迭代对象所有非Symbol键属性(包括原型上的)的键,后者迭代可迭代对象自有属性的值,for...of的迭代顺序是迭代器对象调用next()产生的值的顺序:
Object.prototype.objCustom = function(){};
Array.prototype.arrCustom = function(){};
let a = [4,5,6,,null,undefined,7];
a[2] =8;
a['name'] = 'Ciri';
for(let i in a){
console.log(i);//0,1,2,4,5,6,name,arrCustom,objCustom
}
for(let k in a){
if(a.hasOwnProperty(k)){
console.log(k);//0,1,2,4,5,6,name
}
}
for(let j of a){
console.log(j);//4,5,8,undefined,null,undefined,7
}
注意:在ES2018中,for-of语句被扩展为for-await-of循环,以支持产生Promise的异步可迭代对象。
break和continue
二者都可以和标签语句组合使用以返回到代码特定的位置:
let num = 0;
outermost://表示第一个for循环
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
if (i == 5 && j == 5) {
break outermost;//退出最外层for循环
}
num++;
}
}
console.log(num); // 55
continue也差不多一样:
let num = 0;
outermost:
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
if (i == 5 && j == 5) {
continue outermost;
}
num++;
}
}
console.log(num); // 95
当i,j=5时从最外层开始继续循环,所以少 5 次迭代。