在JS中,一切事物皆对象,运行环境也是对象,就连方法也是对象,在最底层有windows对象。可以说所有的全局变量,方法都是挂载在window对象上的。
当然window也有其他的一些属性alert、confirm等。
this
this.property // window
如上,返回window对象,this指的就是property属性所运行的环境对象。
const person = {
name:'ccg',
dothing(){
console.log(this.name);
}
}
person.dothing();//ccg
如上,this.name在dothing中进行执行,所以this便指向了dothing的运行环境person。
然而this是可变的,可以进行更改指向的
const person = {
name:'ccg',
dothing(){
console.log(this.name);
}
}
const person2 = {name:'ccg2'};
person2.dothing = person.dothing;
person2.dothing();//ccg2
如上,我们将person2的dothing事件指向了person.dothing。运行环境便是person2,所以执行结果是ccg2。
下面是上面例子的拆解,便于理解
const dothing = function () {
console.log(this.name);
}
const person = {
name: 'ccg',
dothing
}
const person2 = { name: 'ccg2' };
person2.dothing = person.dothing;
person2.dothing();//ccg2
注意:函数只要进行变量赋值,那么函数内的this便会更改。
const person = {
dothing:function(){
console.log(this);
}
}
const func = person.dothing;
func();//window
如上,我们将person.dothing事件赋值给了func,因为func的调用方是window,也就是func的运行环境是window,所以它内部的this就是window对象。
<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指向的却是当前的文本对象也就是input框。通过this.value获取输入框的值。
this 的实质
JS之所以有this这个概念,跟内存存储机制有关。
const obj = {name:'ccg'};
如上,一个简单的obj对象。按照语法理解,似乎我们是先声明一个变量obj,然后给obj了一个对象值,其实不然。在JS中是先在内存中创建一个{name:'ccg'},然后将内存地址赋值给obj,那么obj这个时候就指向了{name:'ccg'}。也就是说,obj其实就是一个内存地址。
原始的对象保存是以字典结构保存,每一个属性都有自己的描述对象。如下:
{
name: {
[[value]]: 'ccg'
[[writable]]: true
[[enumerable]]: true
[[configurable]]: true
}
}
对象的没个属性都可以进行描述属性修改,这就是为什么我们使用Object.create()创建对象是第二个参数会以描述对象的形式书写。
关于Object对象https://www.jianshu.com/p/0846f6642769
这样的数据结构似乎看起来更加清晰,但问题是,对象属性有可能是一个方法。
{
name: {
[[value]]: 函数地址
[[writable]]: true
[[enumerable]]: true
[[configurable]]: true
}
}
如上,JS需要在内存中创建一个方法,然后将方法地址绑定给name.value
函数是一个单独的值,JS是允许更改函数的上下文环境的。
function f(){}
const obj = {f}
f();
obj.f()
如上,f是一个方法,我们允许f单独执行,则它的运行环境是window。在obj.f()中它的运行环境便是obj。
那么我们需要一个变量来指明我所运行的上下文,便于我获取我需要的方法或变量,这个时候就有了this关键字,它的目的就是指明当前环境的上下文。
this的运用环境
全局环境
console.log(this === window); //true
function func(){
console.log(this === window);
}
func(); //true
以上代码说明不管实在函数内部还是在外部,只要实在全局变量下,所以的this都指向顶层对象window。
构造函数
在构造函数中this指的是当前实例对象。
function Person(name){this.name = name};
Person('ccg');
const p = new Person('ccg2');
console.log(name); //ccg
conso.log(p.name); //ccg2
如上,Person是一个简单的构造函数。我们将Person当方法调用时,this指向window,便会声明一个全局变量name。我们将Person当构造函数时,this便指向当前实例p,p便有了自己的属性name。
对象的方法
我们都知道,在对象的方法内,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指向全局环境。上面三种情况等同于下面的代码。
const a = {
aName:'a',
b:{
f:function(){
conso.log(this.aName);'
}
}
}
a.b.f(); //undefined
如上,this指向的a.b并不能指向a
我们要么在a.b中添加变量,要么使用变量赋值进行更改this的上下文。
this注意点
由于this是不确定的,尽量不要再方法中使用多重this
const obj = {
name:'',
func:function(){
(function test(){
console.log(this);
})()
}
}
obj.func(); //window
如上,在obj.func中添加一个自执行函数,在自执行函数中this便指向了window,因为自执行函数的调用方式window。
一个解决办法就是在obj.func声明that = this;然后早自执行函数值使用that进行调用obj。但是当我们嵌套多层时需要创建很多个变量来指明上下文,是一件很麻烦的事情。
或者我们可以使用JS的严格模式防止this指向window,在严格模式中禁止this指向window,会报错。
避免再数组处理方法中使用this。
var o = {
v: 'hello',
p: [ 'a1', 'a2' ],
f: function f() {
this.p.forEach(function (item) {
console.log(this.v + ' ' + item);
});
}
}
o.f()
// undefined a1
// undefined a2
上面代码中,foreach方法的回调函数中的this,其实是指向window对象,因此取不到o.v的值。原因跟上一段的多层this是一样的,就是内层的this不指向外部,而指向顶层对象。
解决这个问题的一种方法,就是前面提到的,使用中间变量固定this。
另一种方法是将this当作foreach方法的第二个参数,固定它的运行环境。如下
var o = {
v: 'hello',
p: [ 'a1', 'a2' ],
f: function f() {
this.p.forEach(function (item) {
console.log(this.v + ' ' + item);
}, this);
}
}
o.f()
// hello a1
// hello a2
避免回调中使用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固定指向某个对象,减少不确定性
JS中this的动态绑定,虽然为JS带来了一定的灵活性,但也为初级开发者带来了一定的难度,初学者可能无法快速的定位到this的上下文。有时候我们需要将this固定下来,JS提供了call、apply、bind三个方法专门用于this指定
Function.prototype.call();
函数实例的call方法可以为函数指定运行的作用于,即函数内部this的指向。
function func(){
console.log(this);
}
const obj = {};
func(); //window
func.call(obj);// obj
如上,func是一个全局方法,obj是一个全局变量,我们通过call方法将obj作为参数传递,在func中this便指向了obj。
注意:call方法会在我们设置时自执行。
call方法的参数,是应该是一个对象,当我们没传或者传递null、undefined时,参数默认是window也就是全局对象。如下:
var n = 123;
var obj = { n: 456 };
function a() {
console.log(this.n);
}
a.call() // 123
a.call(null) // 123
a.call(undefined) // 123
a.call(window) // 123
a.call(obj) // 456
如果call中参数是一个原始值,数字或者字符串或者Boolean等,默认会包装成对象传给call
call的第一个参数是需要绑定的对象,从第二个参数开始便是运行函数所需要的参数
function func(a, b) {
this.name = 'test';
console.log(a + b)
};
const obj = { func };
func.call(obj, 1, 2); //3
console.log(obj); //test
如上,我们可以将func定义为一个公共方法来处理我们需要处理的数据对象。
call方法的一个应用是调用对象的原生方法。
var obj = {};
obj.hasOwnProperty('toString') // false
// 覆盖掉继承的 hasOwnProperty 方法
obj.hasOwnProperty = function () {
return true;
};
obj.hasOwnProperty('toString') // true
Object.prototype.hasOwnProperty.call(obj, 'toString') // false
如上,hasOwnProperty是obj对象继承的方法,如果这个方法一旦被覆盖,就不会得到正确结果。call方法可以解决这个问题,它将hasOwnProperty方法的原始定义放到obj对象上执行,这样无论obj上有没有同名方法,都不会影响结果。
Function.prototype.apply();
apply的使用方法基本和call一致,也是用来改变函数运行上下文的方法,区别是apply只有两个参数,第一个参数是需要绑定的Objec,第二个参数是一个数组,数组由函数需要的参数构成。
function f(x, y){
console.log(x + y);
}
f.call(null, 1, 1) // 2
f.apply(null, [1, 1]) // 2
因为apply第二个参数需要穿数组,所以我们可以使用apply做一些特殊的事情。
1.找出数组最大元素
const a = [1,2,45,2,3,5,2314,12,4];
Math.max.apply(null,a); //2314
2.将数组的空元素转换为undefined
Array.apply(null, ['a', ,'b'])
// [ 'a', undefined, 'b' ]
空元素与undefined的区别是我们在forEach时空元素会被跳过,undefined则会被读为undefined。
3.转换疑似数组的对象
Array.prototype.slice.apply({0: 1, length: 1}) // [1]
Array.prototype.slice.apply({0: 1}) // []
Array.prototype.slice.apply({0: 1, length: 2}) // [1, undefined]
Array.prototype.slice.apply({length: 1}) // [undefined]
利用数组的slice属性可以装类似于数组的对象转换成数组(例如arguments)
从上面的代码可以看出,这个方法运行的前提是对象必须有length属性。
4.绑定回调函数的对象。
var o = new Object();
o.f = function () {
console.log(this === o);
}
var f = function (){
o.f.apply(o);
// 或者 o.f.call(o);
};
// jQuery 的写法
$('#button').on('click', f);
Function.prototype.bind()
apply和call方法可以用来改变this的指向,但是它们是立即执行函数,也就是我们放什么时候绑定,什么时候就会自执行一次。
更简洁的办法就是使用bind绑定,bind的传参与call一致,但是它不会立即执行
const obj = {
name:'ccg',
func:function(){
'use strict'
console.log(this.name);
}
}
const print = obj.func;
print();//Uncaught TypeError: Cannot read property 'name' of undefined
如上,我们在将obj.func作为变量进行赋值给print,再执行print会报错,因为在print中this指向的是window,没有name属性。
const print = obj.func.bind(obj);
print(); //ccg
当我们使用bind进行绑定之后便可以使用。且不会立即执行。
bind绑定需要注意:
1.每次绑定都会返回一个方法,会产生一些问题。例如事件绑定时
element.addEventListener('click', o.m.bind(o));
element.removeEventListener('click', o.m.bind(o)); //无效
如上,我们在事件绑定时使用bind,会生成一个匿名函数,并且我们没办法取消绑定。
但是我们可以将事件写成一个变量,如下:
var listener = o.m.bind(o);
element.addEventListener('click', listener);
// ...
element.removeEventListener('click', listener);
2.结合回调函数使用
var counter = {
count: 0,
inc: function () {
'use strict';
this.count++;
}
};
function callIt(callback) {
callback();
}
callIt(counter.inc.bind(counter));
counter.count // 1