[20190507]
- 实例对象与 new 命令
- this 关键字
- 对象的继承
- Object 对象的相关方法
- 严格模式
面向对象编程(Object Oriented Programming,缩写为 OOP)是目前主流的编程范式。它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。
典型的面向对象编程语言(比如 C++ 和 Java),都有“类”(class)这个概念。所谓“类”就是对象的模板,对象就是“类”的实例。但是,JavaScript 语言的对象体系,不是基于“类”的,而是基于构造函数(constructor)和原型链(prototype)。
JavaScript 语言使用构造函数(constructor)作为对象的模板。
构造函数的名字第一个字母大写。
// 构造函数
var Vehicle = function() {
this.price = 1000;
}
var v = new Vehicle();
v.price // 1000
构造函数的特点有两个。
- 函数体内部使用了this关键字,代表了所要生成的对象实例。
- 生成对象的时候,必须使用new命令。
使用new命令时,根据需要,构造函数也可以接受参数。
var Vehicle = function(p) {
this.price = p;
}
var v = new Vehicle(500);
不用new的话,直接调用构造函数的话,等同于直接调普通函数。
一个很自然的问题是,如果忘了使用new命令,直接调用构造函数会发生什么事?
这种情况下,构造函数就变成了普通函数,并不会生成实例对象。而且由于后面会说到的原因,this这时代表全局对象,将造成一些意想不到的结果。
var Vehicle = function (){
this.price = 1000;
};
var v = Vehicle();
v // undefined
price // 1000
严格模式下,可以显示提醒这是个错误用法。
function Fubar(foo, bar){
'use strict';
this._foo = foo;
this._bar = bar;
}
Fubar()
// TypeError: Cannot set property '_foo' of undefined
严格模式下,函数内部的this不能指向全局对象。
更健壮的写法是:
function Fubar(foo, bar) {
if !(this instanceof Fubar) {
return new Fubar(foo, bar);
}
this._foo = foo;
this._bar = bar;
}
Fubar(1, 2)._foo // 1
(new Fubar(1, 2))._foo // 1
执行new的过程:
- 创建一个空对象,作为将要返回的对象实例。
- 将这个空对象的原型,指向构造函数的prototype属性。
- 将这个空对象赋值给函数内部的this关键字。
- 开始执行构造函数内部的代码。
this
指向新生成的空对象,针对this
的操作,都会发生在这个空对象上。
构造函数之所以叫“构造函数”,就是说这个函数的目的,就是操作一个空对象(即this对象),将其“构造”为需要的样子。
如果构造函数内部有return语句,而且return后面跟着一个对象,new命令会返回return语句指定的对象;否则,就会不管return
语句,返回this
对象。
var Vehicle = function () {
this.price = 1000;
return 1000;
};
(new Vehicle()) === 1000
// false
上面代码中,构造函数Vehicle的return语句返回一个数值。这时,new命令就会忽略这个return语句,返回“构造”后的this对象。
但是,如果return语句返回的是一个跟this无关的新对象,new命令会返回这个新对象,而不是this对象。这一点需要特别引起注意。
var Vehicle = function (){
this.price = 1000;
return { price: 2000 };
};
(new Vehicle()).price
// 2000
new.target属性
函数内部可以使用new.target属性。如果当前函数是new命令调用,new.target指向当前函数,否则为undefined。
function f() {
console.log(new.target === f);
}
f() // false
new f() // true
或者:
function f() {
if (!new.target) {
throw new Error('请使用 new 命令调用!');
}
// ...
}
f() // Uncaught Error: 请使用 new 命令调用!
以现有对象为模板生成新的对象: Object.create()
var person1 = {
name: '张三',
age: 38,
greeting: function() {
console.log('Hi! I\'m ' + this.name + '.');
}
};
var person2 = Object.create(person1);
person2.name // 张三
person2.greeting() // Hi! I'm 张三.
this关键字
this
关键字是一个非常重要的语法点。毫不夸张地说,不理解它的含义,大部分开发任务都无法完成。
this
用在构造函数中,表示实例对象。
不管this
用在什么场合,共同点都是:返回一个对象。
简单说,this
就是属性或方法“当前”所在的对象.
var person = {
name: "BING",
describe: function () {
return "name: " + this.name;
}
}
上面代码中,this.name
表示name
属性所在的那个对象。由于this.name
是在describe
方法中调用,而describe
方法所在的当前对象是person
,因此this
指向person
,this.name
就是person.name
。
this指向的可变性
由于对象的属性可以赋给另一个对象,所以属性所在的当前对象是可变的,即this的指向是可变的。
var A = {
name: '张三',
describe: function () {
return '姓名:'+ this.name;
}
};
var B = {
name: '李四'
};
B.describe = A.describe;
B.describe()
// "姓名:李四"
A.describe() // "姓名:张三"
上面代码中,A.describe属性被赋给B,于是B.describe就表示describe方法所在的当前对象是B,所以this.name就指向B.name。
更清晰的表示:
function f() {
return "姓名:" + this.name;
}
var A = {
name: "张三",
describe: f
}
var A = {
name: "李四",
describe: f
}
A.describe() // "姓名:张三"
B.describe() // "姓名:李四"
随着f所在的对象不同,this
的指向也发生变化。
只要函数被赋给另一个变量,this的指向就会变。
下面这个例子更有趣:
var A = {
name: '张三',
describe: function () {
return '姓名:'+ this.name;
}
};
var name = '李四';
var f = A.describe;
f() // "姓名:李四"
上面代码中,A.describe被赋值给变量f,内部的this就会指向f运行时所在的对象(本例是顶层对象)。
网页编程示例:
<input type="text" name="age" size=3 onChange="validate(this, 18, 99);">
<script>
function validate(obj, lowval, hival){
if ((obj.value < lowval) || (obj.value > hival))
console.log('Invalid Value!');
}
</script>
上面代码是一个文本输入框,每当用户输入一个值,就会调用onChange回调函数,验证这个值是否在指定范围。浏览器会向回调函数传入当前对象,因此this就代表传入当前对象(即文本框),然后就可以从this.value上面读到用户的输入值。
总结
JS中一切皆对象,运行环境也是对象,函数是在某个对象中运行,this
就是函数运行时所在的对象。
这个含义是明确的,但是因为JS本身支持环境的动态切换,所以this
的指向是动态的,不能事先确定到底指向哪个对象。
this的实质
var f = function () {
console.log(this.x);
}
var x = 1;
var obj = {
f: f,
x: 2,
};
// 单独执行
f() // 1
// obj 环境执行
obj.f() // 2
现在问题就来了,由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。
this的使用场合
- 全局环境
- 构造函数
- 对象的方法
分类上来说就三个类别。
全局环境
this === window // true
function f() {
console.log(this === window);
}
f() // true
构造函数
var Obj = function(p) {
this.p = p;
}
var o = new Obj("hello world!");
o.p; // "hello world!"
上面代码定义了一个构造函数Obj。由于this指向实例对象,所以在构造函数内部定义this.p,就相当于定义实例对象有一个p属性。
对象的方法
如果对象的方法中包含this
,则this
指向的就是方法运行时所在的对象。
将该方法赋值给另一个对象,就会改变this
的指向。
var obj ={
foo: function () {
console.log(this);
}
};
obj.foo() // obj
下面三种都改变了this
的指向:
// 情况一
(obj.foo = obj.foo)() // window
// 情况二
(false || obj.foo)() // window
// 情况三
(1, obj.foo)() // window
上面代码中,obj.foo就是一个值。这个值真正调用的时候,运行环境已经不是obj了,而是全局环境,所以this不再指向obj。
可以这样理解,JavaScript 引擎内部,obj和obj.foo储存在两个内存地址,称为地址一和地址二。obj.foo()这样调用时,是从地址一调用地址二,因此地址二的运行环境是地址一,this指向obj。但是,上面三种情况,都是直接取出地址二进行调用,这样的话,运行环境就是全局环境,因此this指向全局环境。上面三种情况等同于下面的代码。
// 情况一
(obj.foo = function () {
console.log(this);
})()
// 等同于
(function () {
console.log(this);
})()
// 情况二
(false || function () {
console.log(this);
})()
// 情况三
(1, function () {
console.log(this);
})()
this会指向最靠近的那个对象:
var a = {
p: 'Hello',
b: {
m: function() {
console.log(this.p);
}
}
};
a.b.m() // undefined
改成下面这样即可:
var a = {
b: {
m: function() {
console.log(this.p);
},
p: 'Hello'
}
};
嵌套对象内部的方法赋值给一个变量,会使得this
指向发生转移:
var a = {
b: {
m: function() {
console.log(this.p);
},
p: 'Hello'
}
};
var hello = a.b.m;
hello() // undefined
将嵌套对象内部的方法赋值给一个变量,this依然会指向全局对象。
避免多层this
var o = {
f1: function () {
console.log(this);
var f2 = function () {
console.log(this);
}();
}
}
o.f1()
// Object
// Window
上面代码包含两层this,结果运行后,第一层指向对象o,第二层指向全局对象,因为实际执行的是下面的代码。
var temp = function () {
console.log(this);
};
var o = {
f1: function () {
console.log(this);
var f2 = temp();
}
}
this的传递:
var o = {
f1: function() {
console.log(this);
var that = this;
var f2 = function() {
console.log(that);
}();
}
}
o.f1()
// Object
// Object
上面代码定义了变量that,固定指向外层的this,然后在内层使用that,就不会发生this指向的改变。
事实上,使用一个变量固定this的值,然后内层函数调用这个变量,是非常常见的做法,请务必掌握。
内层this
不指向外部,而是指向了顶层对象。
避免回调函数中的this
var o = new Object();
o.f = function () {
console.log(this === o);
}
// jQuery 的写法
$('#button').on('click', o.f);
上面代码中,点击按钮以后,控制台会显示false。原因是此时this不再指向o对象,而是指向按钮的 DOM 对象,因为f方法是在按钮对象的环境中被调用的。这种细微的差别,很容易在编程中忽视,导致难以察觉的错误。
为了解决这个问题,可以采用下面的一些方法对this进行绑定,也就是使得this固定指向某个对象,减少不确定性。
绑定 this
this
的动态切换,为JS提供了巨大的灵活性,有时需要把this
固定下来。
三种固定方法:
- call
- apply
- bind
call()
var obj = {};
var f = function () {
return this;
};
f() === window // true
f.call(obj) === obj // true
函数实例的call方法,可以指定函数内部this的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数。
注意是在函数实例上调用call
。
function add(a, b) {
return a + b;
}
add.call(this, 1, 2) // 3
call方法还可以接受多个参数,call的第一个参数就是this所要指向的那个对象,后面的参数则是函数调用时所需的参数。
apply()
apply方法的作用与call方法类似,也是改变this指向,然后再调用该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数,使用格式如下。
func.apply(thisValue, [arg1, arg2, ...])
function f(x, y){
console.log(x + y);
}
f.call(null, 1, 1) // 2
f.apply(null, [1, 1]) // 2
apply方法的第一个参数也是this所要指向的那个对象,如果设为null或undefined,则等同于指定全局对象。第二个参数则是一个数组,该数组的所有成员依次作为参数,传入原函数。原函数的参数,在call方法中必须一个个添加,但是在apply方法中,必须以数组形式添加。
求数组的最大元素
var a = [10, 2, 4, 15, 9]
Math.max.apply(null, a);
bind()
var print = d.getTime.bind(d);
print() // 1481869925657
上面代码中,bind方法将getTime方法内部的this绑定到d对象,这时就可以安全地将这个方法赋值给其他变量了。
bind方法的参数就是所要绑定this的对象,下面是一个更清晰的例子。
var counter = {
count: 0,
inc: function () {
this.count++;
}
};
var func = counter.inc.bind(counter);
func();
counter.count // 1
参考
https://wangdoc.com/javascript/oop/new.html
https://wangdoc.com/javascript/oop/this.html