最近阅读 JavaScript高级程序设计(第4版),复习一下JS的基础知识点,发现好多JS知识没有深入了解,写个笔记,记录一下从书中学到的知识。
defineProperty
故名思义,defineProperty是用来定义属性,平时直接定义属性只能定义值,而它能定义数据属性的特性。JS数据属性的特性有:
Configurable: 表示属性是否可修改特性或删除属性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象上的属性的这个特 性都是true。
Enumerable: 表示属性可否遍历,即可否用for…in返回,默认是true。
Writable: 设置可否修改属性的值,默认是true。
Value: 属性实际的值,即平时读写的值,默认是undefined。
用一个例子来说明defineProperty的用法:
let person = {};
Object.defineProperty(person, 'name', {
configurable: true,
writable: false,
value: "tom"
});
console.log(person.name); // tom
person.name = "greg";
console.log(person.name); // tom
defineProperty接收三个参数,第一个是对象本身,第二个是属性名,第三名是设定,要用对象表示。
访问器属性
defineProperty还可以修改对象的访问器,所谓访问器可以理解为读写属性时调用的函数。用一个例子说明用法:
let book = {
year_: 2017,
edition: 1
};
Object.defineProperty(book, "year", {
get() {
return this.year_;
},
set(newValue) {
if (newValue > 2017) {
this.year_ = newValue;
this.edition += newValue - 2017;
}
}
});
book.year = 2018;
console.log(book.edition); // 2
当修改year属性时,执行year的set访问器。
注意一点,使用defineProperty是修改对象本身,如果希望不修改对象,又想为它增加特性,可以使用代理 (Proxy)。
合并对象
如果想合并一个或多个对象为一个,可以使用assign,它的第一个参数为最后合并返回的对象,其余参数为需要合并的对象。合并从第二个参数直到最后一个。举个例:
dest = { id: 'dest' };
result = Object.assign(dest, { id: 'src1', a: 'foo' }, { id: 'src2', b: 'bar' });
// Object.assign 会覆盖重复的属性
console.log(result); // { id: src2, a: foo, b: bar }
如果只是简单想合并对象,也可以使解构来实现:
result = { id: src2, a: foo, b: bar }
const obj = {a:45,b:"hello",...result};
console.log(obj) // {a: "foo", b: "bar", id: "src2"} 会覆盖重复的属性
最后要注意的是,assign只能浅复制,即如果合并对象中的属性是对象或数组,返回的对象的相应属性的指针也是指向该对象或数组。如果想实现深复制,要自己实现或使用lodash等工具函数。
属性值简写
平时创建对象,需要键/值定义,如果希望用变量名为属性名,变量值为属性值,可以使用简写形式:
const name = "Mary";
const o = {name};
console.log(o); // {name: "Mary"}
可计算属性
ES6后,属性名不仅可以静态定义,还可以动态定义:
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let uniqueToken = 0;
function getUniqueKey(key) {
return `${key}_${uniqueToken++}`;
}
let person = {
[getUniqueKey(nameKey)]: 'Matt',
[getUniqueKey(ageKey)]: 27,
[getUniqueKey(jobKey)]: 'Software engineer'
};
console.log(person); // { name_0: 'Matt', age_1: 27, job_2: 'Software engineer' }
我们可以用中括号引用函数或变量,动态定义属性名。
对象解构
如果只是想要读取对象某个属性,可以使用解构:
const obj = {a: 5, b: 6};
let {a} = obj;
console.log(a); // 5
如果解构的属性不是在解构对象中,其值为undefined。为了防止undefined出现,可以设定默认值:
const obj = {a: 5, b: 6};
let {b,f = "hello"} = obj;
console.log(f); // hello
解构可以是嵌套对象:
let person = {
name: 'Matt',
age: 27,
job: {
title: 'Software engineer'
}
};
// 声明title 变量并将person.job.title 的值赋给它
let { job: { title } } = person;
console.log(title); // Software engineer
或是在参数中解构:
let person = {
name: 'Matt',
age: 27
};
function printPerson(foo, {name, age}, bar) {
console.log(arguments);
console.log(name, age);
}
function printPerson2(foo, {name: personName, age: personAge}, bar) {
console.log(arguments);
console.log(personName, personAge);
}
printPerson('1st', person, '2nd');
// ['1st', { name: 'Matt', age: 27 }, '2nd']
// 'Matt', 27
printPerson2('1st', person, '2nd');
// ['1st', { name: 'Matt', age: 27 }, '2nd']
// 'Matt', 27
判断属性存在
如果想判断属性是否存在,可以用in:
"a" in {a:1,b:5} // true
"c" in {a:1,b:5}; // false
不过in的判定包含prototype的属性,如果只是想判断类的属性,应该使用Object.hasOwnProperty:
const Person = function (name, age) {
this.name = name;
this.age = age;
};
Person.prototype.a = "Hello World";
const person1 = new Person("Tom", 56);
console.log("a" in person1); // true
console.log(person1.hasOwnProperty("a")); // false
因此如果只想判断prototype的属性,可以封装以下函数判断:
function hasProtoProperty (obj, property) {
return (property in obj) && (!obj.hasOwnProperty(property));
}
送代
送代是对象里的一个大话题。对象是可以用for循环的,把可以枚举的属性遍历(Enumerable为true):
const Person = function (name, age) {
this.name = name;
this.age = age;
};
Person.prototype.a = "Hello World";
const person1 = new Person("Tom", 56);
for (const item in person1) {
console.log(item,person1[item]);
}
// name Tom
age 56
a Hello World
注意for遍历是会把prototype的属性也遍历,如果不希望遍历prototype的属性,可以利用hasOwnProperty:
...
for (const item in person1) {
if (person1.hasOwnProperty(item)){
console.log(item,person1[item]);
}
}
// name Tom
age 56
另外ES6后Object新增两个方法可以把对象的可枚举属性,而且不是prototype的属性变成数组,从而可以用数组方式送代:values和entries,前者把对象的可枚举属性值转变为数组,后者把属性名与属性值用数组返回,以键值方式表示:
...
console.log(person1.values()) // ["Tom", 56]
console.log(person1.entries()) // [["name", "Tom"], ["age", 56]]
创建对象与原型
儘管JS是面向对象语言,但不同于一般的面向对象语言,它是没有类 (class),ES6加入的class也只是语法糖,JS中通常类是由函数创建,函数才是一等公民。
const Person = function (name, age) {
this.name = name;
this.age = age;
this.sayHello = function () {
console.log(`say hi to ${this.name}`);
}
}
const person1 = new Person("Tom", 11);
创建对象通常要先定义一个函数,然后用new来调用它,表示它是构造对象函数。当new的时候,JS做了以下的事:
(1) 在内存中创建一个新对象。
(2) 这个新对象内部的[[Prototype]]特性被 值为构造函数的 prototype 属性。
(3) 构造函数内部的 this 被 值为这个新对象(即 this 指向新对象)。
(4) 执行构造函数内部的代码(给新对象 加属性)。
(5) 如果构造函数 回非 对象,则 回该对象; 则, 回刚创建的新对象。
如果想更深入了解new,可以查阅手撕JS原生代码。
回来再看一下上述的例子,当再创建一个实例时,sayHello方法又创建一次,事实上我们只希望创建一次,所有实例分享它,原型 (prototype)就是解决这问题的。
function Person (name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function () {
console.log(`say hi to ${this.name}`);
}
const person1 = new Person("Tom", 11);
const person2 = new Person("Mary", 11);
console.log(person1.sayHello === person2.sayHello); // true
当实例调用sayHello时,JS会先查找实例有没有这方法,如果没有就去原型里找。
用 JavaScript高级程序设计(第4版) 概括一下:
继承
原型式继承
利用原型,可以实现类的继承,例如上面的Person实例可以使用Object原型方法,原因是当调用Object方法,JS首先找实例有没有该方法,没有利用 __proto__去找原型方法,如果都没有,则调用Person.proto,它是指向Object.prototype,在里面找相应方法:
function Person (name, age) {
this.name = name;
this.age = age;
}
const person1 = new Person("Tom", 11);
console.log(person1.hasOwnProperty('name')); // true
JS就是通过原型链实现继承。
不过原型链继承有些问题,先看下面的例子:
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {}
// 继承SuperType
SubType.prototype = new SuperType();
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
let instance2 = new SubType();
console.log(instance2.colors); // "red,blue,green,black"
原型中包含的引用值会在所有实例间共享,这也是为什么属性通常会 在构造函数中定义而不会定义在原型上的原因。在使用原型实现继承时,原型实际上变成了另一个类型的实例。这意味着原先的实例属性 身一变成为了原型属性。
原型的第二个问题是,子类型在实例化时不能给类型的构造函数传参。事实上,我们无法在不影响所有对象实例的情况下把参数传进类的构造函数。再加上之前提到的原型中包含引用值的问题,就导致原型基本不会被单独使用。
盗用构造函数 (constructor stealing)
为了解决原型共享问题,盗用构造函数兴起,它的实现很简单:把在子类构造函数中调用类构造函数。因为毕竟函数就是在特定上下文中执行代码的简单对象,所以可以使用apply()和call()方法以新创建的对象为上下文执行构造函数。来看下面的例子:
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {
SuperType.call(this);
}
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
let instance2 = new SubType();
console.log(instance2.colors); // "red,blue,green"
现在解决了父类属性共享的问题,也可以往父类的构造函数传参,但不能使用父类的原型方法。因此盗用构造函数不能单独使用。因此,自然想到把原型链和盗用构造函数结合使用。
组合继承
组合继承把上述两者方法结合:
function SuperType() {
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.showColor = function () {
this.colors.forEach (item => console.log(`color: ${item}`))
}
function SubType() {
SuperType.call(this);
}
SubType.prototype = new SuperType();
let instance1 = new SubType();
instance1.colors.push("black");
instance1.showColor();
/*
color: red
color: blue
color: green
color: black
*/
let instance2 = new SubType();
instance2.showColor();
/*
color: red
color: blue
color: green
*/
问题是解决了,但存在效率问题,其实我们只是第二次创建父类,只是希望使用它的原型方法,没有必要创建两次。 有没有更好的方法?
寄生式组合继承
其实所谓继承希望做的是子类有父类的属性,而且可以调用父类的原型方法。子类继承父类属性可以直接在子类的构造函数调用父类的构造函数。调用父类的原型方法可以把子类的prototype指向父类的prototype,但如果直接指向,则会把constructor指向父类构造函数,而不是子类的,所以还要把constructor指向子类。这问题可以用Object.create解决,它接收对象原型,返回一个新的类,原型指向接收的原型。
因此可以这样实现:
// Shape - 父类(superclass)
function Shape() {
this.x = 0;
this.y = 0;
}
// 父类的方法
Shape.prototype.move = function(x, y) {
this.x += x;
this.y += y;
console.info('Shape moved.');
};
// Rectangle - 子类(subclass)
function Rectangle() {
Shape.call(this); // call super constructor.
}
// 子类续承父类
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;
var rect = new Rectangle();
console.log('Is rect an instance of Rectangle?',
rect instanceof Rectangle); // true
console.log('Is rect an instance of Shape?',
rect instanceof Shape); // true
rect.move(1, 1); // Outputs, 'Shape moved.'
寄生式组合继承可以算是引用类型继承的最佳模式。
当然事实上做项目很少会用到上述的继承,因为ES6后,已经用class语法糖,一个extends已经在JS内部把继承问题解决,但内部依然是利用上述提到的原理解决。了解ES6前的继承方法,可以更好地了解JS的类运行机制。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!