五、构造函数和 new 运算符
1. 构造函数
构造函数的作用在于 实现可重用的对象创建代码 。 通常,对于构造函数有两个约定:
- 命名时首字母大写;
- 只能使用
new
运算符执行。
new
运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。 语法如下:
new constructor[([arguments])]
参数如下:
constructor
一个指定对象实例的类型的类或函数。arguments
一个用于被constructor
调用的参数列表。
2. 简单示例
举个简单示例:
function User (name){ this.name = name; this.isAdmin = false; } const leo = new User('leo'); console.log(leo.name, leo.isAdmin); // "leo" false
3. new 运算符操作过程
当一个函数被使用 new
运算符执行时,它按照以下步骤:
- 一个新的空对象被创建并分配给
this
。 - 函数体执行。通常它会修改
this
,为其添加新的属性。 - 返回
this
的值。
以前面 User
方法为例:
function User(name) { // this = {};(隐式创建) // 添加属性到 this this.name = name; this.isAdmin = false; // return this;(隐式返回) } const leo = new User('leo'); console.log(leo.name, leo.isAdmin); // "leo" false
当我们执行 new User('leo')
时,发生以下事情:
- 一个继承自
User.prototype
的新对象被创建; - 使用指定参数调用构造函数
User
,并将this
绑定到新创建的对象; - 由构造函数返回的对象就是
new
表达式的结果。如果构造函数没有显式返回一个对象,则使用步骤1创建的对象。
需要注意:
- 一般情况下,构造函数不返回值,但是开发者可以选择主动返回对象,来覆盖正常的对象创建步骤;
new User
等同于new User()
,只是没有指定参数列表,即User
不带参数的情况;
let user = new User; // <-- 没有参数 // 等同于 let user = new User();
- 任何函数都可以作为构造器,即都可以使用
new
运算符运行。
4. 构造函数中的方法
在构造函数中,也可以将方法绑定到 this
上:
function User (name){ this.name = name; this.isAdmin = false; this.sayHello = function(){ console.log("hello " + this.name); } } const leo = new User('leo'); console.log(leo.name, leo.isAdmin); // "leo" false leo.sayHello(); // "hello leo"
六、可选链 "?."
详细介绍可以查看 《MDN 可选链操作符》 。
1. 背景介绍
在实际开发中,常常出现下面几种报错情况:
// 1. 对象中不存在指定属性 const leo = {}; console.log(leo.name.toString()); // Uncaught TypeError: Cannot read property 'toString' of undefined // 2. 使用不存在的 DOM 节点属性 const dom = document.getElementById("dom").innerHTML; // Uncaught TypeError: Cannot read property 'innerHTML' of null
在可选链 ?.
出现之前,我们会使用短路操作 &&
运算符来解决该问题:
const leo = {}; console.log(leo && leo.name && leo.name.toString()); // undefined
这种写法的缺点就是 太麻烦了 。
2. 可选链介绍
可选链 ?.
是一种 访问嵌套对象属性的防错误方法 。即使中间的属性不存在,也不会出现错误。 如果可选链 ?.
前面部分是 undefined
或者 null
,它会停止运算并返回 undefined
。
语法:
obj?.prop obj?.[expr] arr?.[index] func?.(args)
** 我们改造前面示例代码:
// 1. 对象中不存在指定属性 const leo = {}; console.log(leo?.name?.toString()); // undefined // 2. 使用不存在的 DOM 节点属性 const dom = document?.getElementById("dom")?.innerHTML; // undefined
3. 使用注意
可选链虽然好用,但需要注意以下几点:
- 不能过度使用可选链;
我们应该只将 ?.
使用在一些属性或方法可以不存在的地方,以上面示例代码为例:
const leo = {}; console.log(leo.name?.toString());
这样写会更好,因为 leo
对象是必须存在,而 name
属性则可能不存在。
- 可选链
?.
之前的变量必须已声明;
在可选链 ?.
之前的变量必须使用 let/const/var
声明,否则会报错:
leo?.name; // Uncaught ReferenceError: leo is not defined
- 可选链不能用于赋值 ;
let object = {}; object?.property = 1; // Uncaught SyntaxError: Invalid left-hand side in assignment
- 可选链访问数组元素的方法 ;
let arrayItem = arr?.[42];
4. 其他情况:?.() 和 ?.[]
需要说明的是 ?.
是一个特殊的语法结构,而不是一个运算符,它还可以与其 ()
和 []
一起使用:
4.1 可选链与函数调用 ?.()
?.()
用于调用一个可能不存在的函数,比如:
let user1 = { admin() { alert("I am admin"); } } let user2 = {}; user1.admin?.(); // I am admin user2.admin?.();
?.()
会检查它左边的部分:如果 admin 函数存在,那么就调用运行它(对于 user1
)。否则(对于 user2
)运算停止,没有错误。
4.2 可选链和表达式 ?.[]
?.[]
允许从一个可能不存在的对象上安全地读取属性。
let user1 = { firstName: "John" }; let user2 = null; // 假设,我们不能授权此用户 let key = "firstName"; alert( user1?.[key] ); // John alert( user2?.[key] ); // undefined alert( user1?.[key]?.something?.not?.existing); // undefined
5. 可选链 ?.
语法总结
可选链 ?.
语法有三种形式:
obj?.prop
—— 如果obj
存在则返回obj.prop
,否则返回undefined
。obj?.[prop]
—— 如果obj
存在则返回obj[prop]
,否则返回undefined
。obj?.method()
—— 如果obj
存在则调用obj.method()
,否则返回undefined
。
正如我们所看到的,这些语法形式用起来都很简单直接。?.
检查左边部分是否为 null/undefined
,如果不是则继续运算。 ?.
链使我们能够安全地访问嵌套属性。
七、Symbol
规范规定,JavaScript 中对象的属性只能为 字符串类型 或者 Symbol类型 ,毕竟我们也只见过这两种类型。
1. 概念介绍
ES6引入Symbol
作为一种新的原始数据类型,表示独一无二的值,主要是为了防止属性名冲突。 ES6之后,JavaScript一共有其中数据类型:Symbol
、undefined
、null
、Boolean
、String
、Number
、Object
。 简单使用:
let leo = Symbol(); typeof leo; // "symbol"
Symbol 支持传入参数作为 Symbol 名,方便代码调试: **
let leo = Symbol("leo");
2. 注意事项**
Symbol
函数不能用new
,会报错。
由于Symbol
是一个原始类型,不是对象,所以不能添加属性,它是类似于字符串的数据类型。
let leo = new Symbol() // Uncaught TypeError: Symbol is not leo constructor
Symbol
都是不相等的,即使参数相同。
// 没有参数 let leo1 = Symbol(); let leo2 = Symbol(); leo1 === leo2; // false // 有参数 let leo1 = Symbol('leo'); let leo2 = Symbol('leo'); leo1 === leo2; // false
Symbol
不能与其他类型的值计算,会报错。
let leo = Symbol('hello'); leo + " world!"; // 报错 `${leo} world!`; // 报错
Symbol
不能自动转换为字符串,只能显式转换。
let leo = Symbol('hello'); alert(leo); // Uncaught TypeError: Cannot convert a Symbol value to a string String(leo); // "Symbol(hello)" leo.toString(); // "Symbol(hello)"
Symbol
可以转换为布尔值,但不能转为数值:
let a1 = Symbol(); Boolean(a1); !a1; // false Number(a1); // TypeError a1 + 1 ; // TypeError
Symbol
属性不参与for...in/of
循环。
let id = Symbol("id"); let user = { name: "Leo", age: 30, [id]: 123 }; for (let key in user) console.log(key); // name, age (no symbols) // 使用 Symbol 任务直接访问 console.log( "Direct: " + user[id] );
3. 字面量中使用 Symbol 作为属性名
在对象字面量中使用 Symbol
作为属性名时,需要使用 方括号 ( []
),如 [leo]: "leo"
。 好处:防止同名属性,还有防止键被改写或覆盖。
let leo = Symbol(); // 写法1 let user = {}; user[leo] = 'leo'; // 写法2 let user = { [leo] : 'leo' } // 写法3 let user = {}; Object.defineProperty(user, leo, {value : 'leo' }); // 3种写法 结果相同 user[leo]; // 'leo'
需要注意 :Symbol作为对象属性名时,不能用点运算符,并且必须放在方括号内。
let leo = Symbol(); let user = {}; // 不能用点运算 user.leo = 'leo'; user[leo] ; // undefined user['leo'] ; // 'leo' // 必须放在方括号内 let user = { [leo] : function (text){ console.log(text); } } user[leo]('leo'); // 'leo' // 上面等价于 更简洁 let user = { [leo](text){ console.log(text); } }
常常还用于创建一组常量,保证所有值不相等:
let user = {}; user.list = { AAA: Symbol('Leo'), BBB: Symbol('Robin'), CCC: Symbol('Pingan') }
4. 应用:消除魔术字符串
魔术字符串:指代码中多次出现,强耦合的字符串或数值,应该避免,而使用含义清晰的变量代替。
function fun(name){ if(name == 'leo') { console.log('hello'); } } fun('leo'); // 'hello' 为魔术字符串
常使用变量,消除魔术字符串:
let obj = { name: 'leo' }; function fun(name){ if(name == obj.name){ console.log('hello'); } } fun(obj.name); // 'hello'
使用Symbol
消除强耦合,使得不需关系具体的值:
let obj = { name: Symbol() }; function fun (name){ if(name == obj.name){ console.log('hello'); } } fun(obj.name); // 'hello'
5. 属性名遍历
Symbol作为属性名遍历,不出现在for...in
、for...of
循环,也不被Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
返回。
let leo = Symbol('leo'), robin = Symbol('robin'); let user = { [leo]:'18', [robin]:'28' } for(let k of Object.values(user)){console.log(k)} // 无输出 let user = {}; let leo = Symbol('leo'); Object.defineProperty(user, leo, {value: 'hi'}); for(let k in user){ console.log(k); // 无输出 } Object.getOwnPropertyNames(user); // [] Object.getOwnPropertySymbols(user); // [Symbol(leo)]
Object.getOwnPropertySymbols
方法返回一个数组,包含当前对象所有用做属性名的Symbol值。
let user = {}; let leo = Symbol('leo'); let pingan = Symbol('pingan'); user[leo] = 'hi leo'; user[pingan] = 'hi pingan'; let obj = Object.getOwnPropertySymbols(user); obj; // [Symbol(leo), Symbol(pingan)]
另外可以使用Reflect.ownKeys
方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。
let user = { [Symbol('leo')]: 1, age : 2, address : 3, } Reflect.ownKeys(user); // ['age', 'address',Symbol('leo')]
由于Symbol值作为名称的属性不被常规方法遍历获取,因此常用于定义对象的一些非私有,且内部使用的方法。
6. Symbol.for()、Symbol.keyFor()
6.1 Symbol.for()
用于重复使用一个Symbol值,接收一个字符串作为参数,若存在用此参数作为名称的Symbol值,返回这个Symbol,否则新建并返回以这个参数为名称的Symbol值。
let leo = Symbol.for('leo'); let pingan = Symbol.for('leo'); leo === pingan; // true
Symbol()
和 Symbol.for()
区别:
Symbol.for('leo') === Symbol.for('leo'); // true Symbol('leo') === Symbol('leo'); // false
6.2 Symbol.keyFor()
用于返回一个已使用的Symbol类型的key:
let leo = Symbol.for('leo'); Symbol.keyFor(leo); // 'leo' let leo = Symbol('leo'); Symbol.keyFor(leo); // undefined
7. 内置的Symbol值
ES6提供11个内置的Symbol值,指向语言内部使用的方法:
7.1 Symbol.hasInstance
当其他对象使用instanceof
运算符,判断是否为该对象的实例时,会调用这个方法。比如,foo instanceof Foo
在语言内部,实际调用的是Foo[Symbol.hasInstance](foo)
。
class P { [Symbol.hasInstance](a){ return a instanceof Array; } } [1, 2, 3] instanceof new P(); // true
P是一个类,new P()会返回一个实例,该实例的Symbol.hasInstance
方法,会在进行instanceof
运算时自动调用,判断左侧的运算子是否为Array
的实例。
7.2 Symbol.isConcatSpreadable
值为布尔值,表示该对象用于Array.prototype.concat()
时,是否可以展开。
let a = ['aa','bb']; ['cc','dd'].concat(a, 'ee'); // ['cc', 'dd', 'aa', 'bb', 'ee'] a[Symbol.isConcatSpreadable]; // undefined let b = ['aa','bb']; b[Symbol.isConcatSpreadable] = false; ['cc','dd'].concat(b, 'ee'); // ['cc', 'dd',[ 'aa', 'bb'], 'ee']
7.3 Symbol.species
指向一个构造函数,在创建衍生对象时会使用,使用时需要用get
取值器。
class P extends Array { static get [Symbol.species](){ return this; } }
解决下面问题:
// 问题: b应该是 Array 的实例,实际上是 P 的实例 class P extends Array{} let a = new P(1,2,3); let b = a.map(x => x); b instanceof Array; // true b instanceof P; // true // 解决: 通过使用 Symbol.species class P extends Array { static get [Symbol.species]() { return Array; } } let a = new P(); let b = a.map(x => x); b instanceof P; // false b instanceof Array; // true
7.4 Symbol.match
当执行str.match(myObject)
,传入的属性存在时会调用,并返回该方法的返回值。
class P { [Symbol.match](string){ return 'hello world'.indexOf(string); } } 'h'.match(new P()); // 0
7.5 Symbol.replace
当该对象被String.prototype.replace
方法调用时,会返回该方法的返回值。
let a = {}; a[Symbol.replace] = (...s) => console.log(s); 'Hello'.replace(a , 'World') // ["Hello", "World"]
7.6 Symbol.hasInstance
当该对象被String.prototype.search
方法调用时,会返回该方法的返回值。
class P { constructor(val) { this.val = val; } [Symbol.search](s){ return s.indexOf(this.val); } } 'hileo'.search(new P('leo')); // 2
7.7 Symbol.split
当该对象被String.prototype.split
方法调用时,会返回该方法的返回值。
// 重新定义了字符串对象的split方法的行为 class P { constructor(val) { this.val = val; } [Symbol.split](s) { let i = s.indexOf(this.val); if(i == -1) return s; return [ s.substr(0, i), s.substr(i + this.val.length) ] } } 'helloworld'.split(new P('hello')); // ["hello", ""] 'helloworld'.split(new P('world')); // ["", "world"] 'helloworld'.split(new P('leo')); // "helloworld"
7.8 Symbol.iterator
对象进行for...of
循环时,会调用Symbol.iterator
方法,返回该对象的默认遍历器。
class P { *[Symbol.interator]() { let i = 0; while(this[i] !== undefined ) { yield this[i]; ++i; } } } let a = new P(); a[0] = 1; a[1] = 2; for (let k of a){ console.log(k); }
7.9.Symbol.toPrimitive
该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。调用时,需要接收一个字符串参数,表示当前运算模式,运算模式有:
- Number : 此时需要转换成数值
- String : 此时需要转换成字符串
- Default : 此时可以转换成数值或字符串
let obj = { [Symbol.toPrimitive](hint) { switch (hint) { case 'number': return 123; case 'string': return 'str'; case 'default': return 'default'; default: throw new Error(); } } }; 2 * obj // 246 3 + obj // '3default' obj == 'default' // true String(obj) // 'str'
7.10 Symbol.toStringTag
在该对象上面调用Object.prototype.toString
方法时,如果这个属性存在,它的返回值会出现在toString
方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制[object Object
]或[object Array]
中object
后面的那个字符串。
// 例一 ({[Symbol.toStringTag]: 'Foo'}.toString()) // "[object Foo]" // 例二 class Collection { get [Symbol.toStringTag]() { return 'xxx'; } } let x = new Collection(); Object.prototype.toString.call(x) // "[object xxx]"
7.11 Symbol.unscopables
该对象指定了使用with关键字时,哪些属性会被with环境排除。
// 没有 unscopables 时 class MyClass { foo() { return 1; } } var foo = function () { return 2; }; with (MyClass.prototype) { foo(); // 1 } // 有 unscopables 时 class MyClass { foo() { return 1; } get [Symbol.unscopables]() { return { foo: true }; } } var foo = function () { return 2; }; with (MyClass.prototype) { foo(); // 2 }
上面代码通过指定Symbol.unscopables
属性,使得with
语法块不会在当前作用域寻找foo
属性,即foo
将指向外层作用域的变量。
八、原始值转换
前面复习到字符串、数值、布尔值等的转换,但是没有讲到对象的转换规则,这部分就一起看看:。 需要记住几个规则:
- 所有对象在布尔上下文中都为
true
,并且不存在转换为布尔值的操作,只有字符串和数值转换有。 - 数值转换发生在对象相减或应用数学函数时。如
Date
对象可以相减,如date1 - date2
结果为两个时间的差值。 - 在字符串转换,通常出现在如
alert(obj)
这种形式。
当然我们可以使用特殊的对象方法,对字符串和数值转换进行微调。下面介绍三个类型(hint)转换情况:
1. object to string
对象到字符串的转换,当我们对期望一个字符串的对象执行操作时,如 “alert”:
// 输出 alert(obj); // 将对象作为属性键 anotherObj[obj] = 123;
2. object to number
对象到数字的转换,例如当我们进行数学运算时:
// 显式转换 let num = Number(obj); // 数学运算(除了二进制加法) let n = +obj; // 一元加法 let delta = date1 - date2; // 小于/大于的比较 let greater = user1 > user2;
3. object to default
少数情况下,当运算符“不确定”期望值类型时。 例如,二进制加法 +
可用于字符串(连接),也可以用于数字(相加),所以字符串和数字这两种类型都可以。因此,当二元加法得到对象类型的参数时,它将依据 "default"
来对其进行转换。 此外,如果对象被用于与字符串、数字或 symbol 进行 ==
比较,这时到底应该进行哪种转换也不是很明确,因此使用 "default"
。
// 二元加法使用默认 hint let total = obj1 + obj2; // obj == number 使用默认 hint if (user == 1) { ... };
4. 类型转换算法
为了进行转换,JavaScript 尝试查找并调用三个对象方法:
- 调用
obj[Symbol.toPrimitive](hint)
—— 带有 symbol 键Symbol.toPrimitive
(系统 symbol)的方法,如果这个方法存在的话, - 否则,如果 hint 是
"string"
—— 尝试obj.toString()
和obj.valueOf()
,无论哪个存在。 - 否则,如果 hint 是
"number"
或"default"
—— 尝试obj.valueOf()
和obj.toString()
,无论哪个存在。
5. Symbol.toPrimitive
详细介绍可阅读《MDN | Symbol.toPrimitive》 。 Symbol.toPrimitive
是一个内置的 Symbol 值,它是作为对象的函数值属性存在的,当一个对象转换为对应的原始值时,会调用此函数。 简单示例介绍:
let user = { name: "Leo", money: 9999, [Symbol.toPrimitive](hint) { console.log(`hint: ${hint}`); return hint == "string" ? `{name: "${this.name}"}` : this.money; } }; alert(user); // 控制台:hint: string 弹框:{name: "John"} alert(+user); // 控制台:hint: number 弹框:9999 alert(user + 1); // 控制台:hint: default 弹框:10000
6. toString/valueOf
toString
/ valueOf
是两个比较早期的实现转换的方法。当没有 Symbol.toPrimitive
,那么 JavaScript 将尝试找到它们,并且按照下面的顺序进行尝试:
- 对于 “string” hint,
toString -> valueOf
。 - 其他情况,
valueOf -> toString
。
这两个方法必须返回一个原始值。如果 toString
或 valueOf
返回了一个对象,那么返回值会被忽略。默认情况下,普通对象具有 toString
和 valueOf
方法:
toString
方法返回一个字符串"[object Object]"
。valueOf
方法返回对象自身。
简单示例介绍:
const user = {name: "Leo"}; alert(user); // [object Object] alert(user.valueOf() === user); // true
我们也可以结合 toString
/ valueOf
实现前面第 5 点介绍的 user
对象:
let user = { name: "Leo", money: 9999, // 对于 hint="string" toString() { return `{name: "${this.name}"}`; }, // 对于 hint="number" 或 "default" valueOf() { return this.money; } }; alert(user); // 控制台:hint: string 弹框:{name: "John"} alert(+user); // 控制台:hint: number 弹框:9999 alert(user + 1); // 控制台:hint: default 弹框:10000
总结
本文作为《初中级前端 JavaScript 自测清单》第二部分,介绍的内容以 JavaScript 对象为主,其中有让我眼前一亮的知识点,如 Symbol.toPrimitive
方法。我也希望这个清单能帮助大家自测自己的 JavaScript 水平并查缺补漏,温故知新。