对象可以表示数据和功能。
构造函数
构造函数:结构和语法
function SoftwareDeveloper() {
this.favoriteLanguage = 'JavaScript';
}
首先,构造函数并不声明局部变量
,而是使用 this 关键字来保存数据
。以上函数将为所创建的任何对象添加一个 favoriteLanguage 属性
,并为其分配一个默认值 'JavaScript'
。现在不用太在意构造函数中的 this
;只要知道 this
是指在构造函数前面使用 new 关键字创建的新对象
即可。
最后一点比较特别的是,这个函数似乎不会返回任何东西
!JavaScript 中的构造函数不应该有一个显式的返回值
(即使用 return
语句)。
创建一个新的对象
function SoftwareDeveloper() {
this.favoriteLanguage = 'JavaScript';
}
let developer = new SoftwareDeveloper();
console.log(developer);
// SoftwareDeveloper {this.favoriteLanguage: "JavaScript"}
function SoftwareDeveloper(name) {
this.favoriteLanguage = 'JavaScript';
this.name = name;
}
let instructor = new SoftwareDeveloper('Andrew');
console.log(instructor);
// SoftwareDeveloper { favoriteLanguage: 'JavaScript', name: 'Andrew' }
注意,大写构造函数名称的第一个字母只是一个命名惯例
。虽然第一个字母应该大写,但如果不小心用了小写,也仍然还是构造函数(即当用 new 运算符调用时,等等)。
查看对象的构造函数 (instanceOf
)
function Developer(name) {
this.name = name;
}
const dev = new Developer('Veronika');
typeof dev // "object"
dev instanceOf Developer; // true
当在对象上调用方法时,this
会被赋值,并且其值指向该对象。由于它是一个保留字,因此不应该用作任何变量名称、函数名称等。
关键字 this
根据 this
的调用方式,分析 this
指代的含义:
如果使用 new 运算符来调用构造函数,this
的值将被设置为新创建的对象。如果在对象上调用方法,this
将被设置为该对象本身。如果简单地调用一个函数,this
将被设置为全局对象:window
。
设置自己的 this
值
Javascript 提供了几种设置 this
值的方法,这些方法分别是 call()
、apply()
和 bind()
。前两种方法在函数上被调用
,会因为参数的传入方式
不同而有所不同。第三种方法是返回新函数
的方法,每种方法都在不同的环境中使用。
call()
call()
是一个直接调用到函数上的方法。我们传递给它一个单一的值,以设置为 this
的值,然后逐个传入该函数的任何参数,用逗号分隔。
function multiply(n1, n2) {
return n1 * n2;
}
multiply(3, 4);
// 12
multiply.call(window, 3, 4);
// 12
除了调用常规函数
之外,我们如何调用附加到对象上的函数
(即方法)呢?
使用 call()
来调用方法允许我们从对象中“借用”方法
,然后将其用于另一个对象
!
const mockingbird = {
title: 'To Kill a Mockingbird',
describe: function () {
console.log(`${this.title} is a classic novel`);
}
};
mockingbird.describe();
// 'To Kill a Mockingbird is a classic novel'
const pride = {
title: 'Pride and Prejudice'
};
mockingbird.describe.call(pride);
// 'Pride and Prejudice is a classic novel'
首先,call()
方法被调用到 mockingbird.describe
(它指向一个函数)上。然后,this
的值被传递给 call()
方法:pride
。
由于 mockingbird
的 describe()
方法引用了 this.title
,我们需要访问 this
所指向的对象的 title
属性。但是,由于我们已经设置了自己的 this
的值,this.title
的值将会从 pride
对象中被访问!结果,mockingbird.describe.call(pride);
被执行,我们在控制台中看到 'Pride and Prejudice is a classic novel'
。
apply()
multiply.apply(window, [3, 4]); // 将函数的参数放在一个数组中
// 12
就像 call()
一样,apply()
在一个函数上被调用,不仅可以调用该函数,而且还可以为它关联一个特定的 this
值。但是,apply()
并不是逐个传递参数并用逗号分隔,而是将函数的参数放在一个数组中
。
mockingbird.describe.apply(pride);
// 'Pride and Prejudice is a classic novel'
传递给 call()
和 apply()
的第一个参数是相同的(即绑定 this
值的对象)。由于 describe()
方法不接受任何参数,因此 mockingbird.describe.call(pride);
和 mockingbird.describe.apply(pride);
唯一的区别就是方法!这两种方法都会产生相同的结果。
偏向选择其中一种方法
如果你事先并不知道函数所需要的参数个数
,那么 call()
的使用可能会受到限制。在这种情况下,apply()
是一个更好的选择
,因为它只接受一个参数数组,然后将其解包并传递给函数。请记住,解包
可能会略微影响性能,但这种影响并不显著。
回调和 this
function invokeTwice(cb) {
cb();
cb();
}
const dog = {
age: 5,
growOneYear: function () {
this.age += 1;
}
}
dog.growOneYear();
dog.age; // 6
invokeTwice(dog.growOneYear); //函数来调用它,因此 this 设成了全局变量,而不是 dog 对象
dog.age; // 6 , dog 的 age 属性没有发生变化
使用匿名闭包来保存 this
简单地调用一个普通函数会将 this
的值设置为全局对象
(即 window)。我们如何解决这个问题呢?
解决这个问题的一种方式就是使用一个匿名闭包来遮蔽 dog
对象:
invokeTwice(function () {
dog.growOneYear();
});
dog.age
// 7
使用 bind() 来保存 this
与 call()
和 apply()
类似,bind()
方法也允许用户直接为 this
定义一个值。bind()
也是一个在函数上调用的方法,但不同于 call()
或 apply()
,它们都会立即调用函数——bind()
会返回一个新的函数
。当被调用时,该函数会将 this
设置为我们赋给它的值。
function invokeTwice(cb) {
cb();
cb();
}
const dog = {
age: 5,
growOneYear: function () {
this.age += 1;
}
}
invokeTwice(dog.growOneYear);
const myGrow = dog.growOneYear.bind(dog);
invokeTwice(myGrow);
dog.age; // 7
bind()
是可以直接在函数中调用
的方法,返回该函数的副本
,并具有特定的 this
值。
原型继承
Cat()
构造函数是使用 new
运算符来调用的,该运算符创建了 bailey
实例(对象)。请注意,meow()
方法是在 bailey
对象的构造函数的原型中定义的。原型只是一个对象,该构造函数所创建的所有对象均被秘密链接到该原型。因此,我们可以将 bailey.meow()
当作 bailey
自身的方法执行。
无论你是访问属性(例如 bailey.lives;
)还是调用方法(即 bailey.meow();
),JavaScript 解释器都会按照特定的顺序在原型链中查找它们:
- 首先,JavaScript 引擎将查看对象自身的属性。这意味着,直接在该对象中定义的任何属性和方法将优先于其他位置的任何同名属性和方法(类似于作用域链中的变量阴影)。
- 如果找不到目标属性,它将搜索对象的构造函数的原型,以寻找匹配。
- 如果原型中不存在该属性,则 JavaScript 引擎将沿着该链继续查找。
- 该链的最顶端是
Object()
对象,也就是顶级父对象。如果_仍然_找不到该属性,则该属性为未定义。
每个函数都有一个 prototype
属性,它其实只是一个对象。当使用 new
运算符将该函数作为构造函数来调用时,它会创建并返回一个新的对象。该对象被秘密地链接到其构造函数的 prototype
,而这个秘密链接让该对象可以访问 prototype
的属性和方法,就像它自己的一样!
function Dog(age, weight, name) {
this.age = age;
this.weight = weight;
this.name = name;
/**
this.bark = function() {
console.log(`${this.name} says woof!`);
};
*/
}
// 在 dog 原型中定义 bark
Dog.prototype.bark = function() {
console.log(`${this.name} says woof!`);
};
dog = new Dog(2, 60, 'Java');
dog.bark(); // Java says woof!
当我们在 Dog
中叫 bark
方法时,JavaScript 引擎会查看自己的属性,尝试找到与 bark
方法相匹配的名称,由于 bark
没有直接定义在这个 dog
上,它会看看 bark
方法的原型,最后我们不需要调用 dog.prototype.bark
,只需要调用 dog.bark
就可以了。因为这个 dog
对象已经通过它的原型与 bark
方法联系起来。
替换 prototype
对象
function Hamster() { this.hasFur = true; }
let waffle = new Hamster();
let pancake = new Hamster();
在创建新的对象 waffle
和 pancake
之后,我们仍然可以为 Hamster
的原型添加属性,而且它仍然可以访问这些新的属性。
Hamster.prototype.eat = function () { console.log('Chomp chomp chomp!'); };
waffle.eat(); // 'Chomp chomp chomp!'
pancake.eat(); // 'Chomp chomp chomp!'
我们将 Hamster
的 prototype
对象完全替换为其他内容:
Hamster.prototype = {
isHungry: false,
color: 'brown'
};
先前的对象无法访问更新后的原型的属性;它们只会保留与旧原型的秘密链接:
console.log(waffle.color); // undefined
waffle.eat(); // 'Chomp chomp chomp!'
console.log(pancake.isHungry); // undefined
事实证明,此后创建的任何新的 Hamster
对象都会使用更新后的原型:
const muffin = new Hamster();
muffin.eat(); // TypeError: muffin.eat is not a function
console.log(muffin.isHungry); // false
console.log(muffin.color); // 'brown'
检查对象的属性
hasOwnProperty()
hasOwnProperty()
可以帮助你找到某个特定属性的来源
。在向其传入你要查找的属性名称的字符串
后,该方法会返回一个布尔值
,指示该属性是否属于该对象本身
(即该属性不是被继承的)。
function Phone() {
this.operatingSystem = 'Android';
}
Phone.prototype.screenSize = 6;
const myPhone = new Phone();
const own = myPhone.hasOwnProperty('operatingSystem');
console.log(own);
//true
const inherited = myPhone.hasOwnProperty('screenSize');
console.log(inherited);
//false
isPrototypeOf()
对象还可以访问 isPrototypeOf()
方法,该方法可以检查某个对象是否存在于另一个对象的原型链中
。 使用这种方法,你可以确认某个特定的对象是否是另一个对象的原型
。
const rodent = {
favoriteFood: 'cheese',
hasTail: true
};
function Mouse() {
this.favoriteFood = 'cheese';
}
Mouse.prototype = rodent;
const ralph = new Mouse();
const result = rodent.isPrototypeOf(ralph)
console.log(result);
//true
创建一个新的 Mouse
对象,它的原型应该是 rodent
对象。isPrototypeOf()
是确认某个对象是否存在于另一个对象的原型链中
的好办法。
Object.getPrototypeOf()
const myPrototype = Object.getPrototypeOf(ralph);
console.log(myPrototype);
//{ favoriteFood: "cheese", hasTail: true }
ralph
的原型与结果具有相同的属性,因为它们就是同一个对象。 Object.getPrototypeOf()
很适合检索给定对象的原型
。
const capitals = {
California: 'Sacramento',
Washington: 'Olympia',
Oregon: 'Salem',
Texas: 'Austin'
};
Object.getPrototypeOf(capitals) === Object.prototype
// true
对象字面量表示法创建的对象,它的构造函数就是内置的 Object()
构造函数本身!因此,它会保持一个对其构造函数原型的引用
。
isPrototypeOf()
可以检查某个对象是否存在于另一个对象的原型链中isPrototypeOf()
会接受一个参数,一个原型链将被搜索的对象getPrototypeOf()
会返回传递给它的对象的原型
constructor 属性
每次创建一个对象时,都会有一个特殊的属性被暗中分配给它:constructor
。访问一个对象的 constructor
属性会返回一个对创建该对象的构造函数的引用
!
function Longboard() {
this.material = 'bamboo';
}
const board = new Longboard();
console.log(board.constructor);
//function Longboard() {
// this.material = 'bamboo';
//}
如果某个对象是使用字面量表示法创建的,那么它的构造函数就是内置的 Object()
构造函数!
const rodent = {
teeth: 'incisors',
hasTail: true
};
console.log(rodent.constructor);
//function Object() { [native code] }
- 访问一个对象的
constructor
属性会返回一个对创建该对象(实例)的构造函数的引用 - 每个对象都有一个
constructor
属性 - 使用字面量表示法创建的对象是用
Object()
构造函数创建的
hasOwnProperty()
isPrototypeOf()
Object.getPrototypeOf()
.constructor