Maison >interface Web >js tutoriel >Bases avancées du front-end (5) : interprétation complète de ceci
Dans le processus d'apprentissage de JavaScript, parce que nous ne comprenons pas très clairement certains concepts, mais que nous voulons les écrire d'une certaine manière, il est facile d'écrire ces concepts à la hâte. des conclusions biaisées qui conviennent à sa propre mémoire.
Ce qui est plus dommageable, c'est que certaines conclusions inexactes circulent largement sur Internet.
Par exemple, dans la compréhension de ce que cela indique, il y a un dicton : celui qui l'appelle, cela indique. Quand j’ai commencé à apprendre cela, je croyais beaucoup en cette phrase. Parce que dans certains cas, cette compréhension a du sens. Mais je rencontre souvent des situations différentes au cours du développement. Un appel incorrect à cause de cela peut me rendre confus pendant une journée entière. À ce moment-là, j'ai également recherché des informations et interrogé des experts du groupe, mais je n'arrivais toujours pas à comprendre « Où ai-je fait une erreur ? En fait, c’est simplement parce que j’ai une conclusion inexacte en tête.
Voici une plainte concernant la recherche Baidu. De nombreux articles trouvés sont erronés, ce qui a longtemps nui aux syndicats et à la direction
Donc, je pense qu'il y en a. la nécessité d'un tel article Article pour vous aider à comprendre cela de manière globale. Que chacun en ait une compréhension correcte et complète.
Avant cela, nous devons revoir le contexte d’exécution.
Dans les articles précédents, j'ai mentionné le cycle de vie du contexte d'exécution à plusieurs endroits. Au cas où vous ne vous en souvenez pas, revoyons-le à nouveau, comme indiqué ci-dessous.
Dans la phase de création du contexte d'exécution, des objets variables seront générés respectivement, la chaîne de portée sera établie et ce point sera déterminé. Nous avons soigneusement résumé les objets variables et les chaînes de portée, et la clé ici est d'en déterminer l'intérêt.
Ici, nous devons tirer une conclusion très importante qu'il faut garder à l'esprit. Le point de ceci est déterminé lorsque la fonction est appelée. Autrement dit, il est déterminé au moment où le contexte d'exécution est créé. Par conséquent, nous pouvons facilement comprendre que le pointeur this dans une fonction peut être très flexible. Par exemple, dans l'exemple suivant, la même fonction pointe vers différents objets en raison de différentes méthodes d'appel.
var a = 10; var obj = { a: 20 } function fn () { console.log(this.a); } fn(); // 10 fn.call(obj); // 20
De plus, lors de l'exécution de la fonction, une fois celle-ci déterminée, elle ne peut plus être modifiée.
var a = 10; var obj = { a: 20 } function fn () { this = obj; // 这句话试图修改this,运行后会报错 console.log(this.a); } fn();
1. ceci dans l'objet global
Concernant celui de l'objet global, je l'ai déjà évoqué en résumant l'objet variable. C'est une existence relativement particulière. Ceci, dans l’environnement mondial, va de soi. C'est donc relativement simple et il n'y a pas beaucoup de complications à prendre en compte.
// 通过this绑定到全局对象 this.a2 = 20; // 通过声明绑定到变量对象,但在全局环境中,变量对象就是它自身 var a1 = 10; // 仅仅只有赋值操作,标识符会隐式绑定到全局对象 a3 = 30; // 输出结果会全部符合预期 console.log(a1); console.log(a2); console.log(a3);
2. ceci dans la fonction
Avant de résumer ce point dans la fonction, je pense que nous devons passer par quelques exemples étranges pour ressentir le caractère insaisissable de cela dans la fonction.
// demo01 var a = 20; function fn() { console.log(this.a); } fn();
// demo02 var a = 20; function fn() { function foo() { console.log(this.a); } foo(); } fn();
// demo03 var a = 20; var obj = { a: 10, c: this.a + 20, fn: function () { return this.a; } } console.log(obj.c); console.log(obj.fn());
Les lecteurs doivent prendre le temps d'expérimenter ces exemples. Si vous ne comprenez pas ce qui se passe, ne vous inquiétez pas, analysons-le petit à petit. .
Avant l’analyse, tirons directement la conclusion.
Dans un contexte de fonction, ceci est fourni par l'appelant et est déterminé par la manière dont la fonction est appelée. Si la fonction appelante appartient à un objet, alors lorsque la fonction est appelée, l'interne this pointe vers l'objet. Si la fonction est appelée indépendamment, alors celle-ci à l'intérieur de la fonction pointe vers indéfini. Mais en mode non strict, lorsque cela pointe vers undefined, il pointera automatiquement vers l'objet global.
De la conclusion, nous pouvons voir que pour déterminer avec précision l'intérêt de cela, il est très important de trouver l'appelant de la fonction et de distinguer s'il appelle indépendamment.
// 为了能够准确判断,我们在函数内部使用严格模式,因为非严格模式会自动指向全局 function fn() { 'use strict'; console.log(this); } fn(); // fn是调用者,独立调用 window.fn(); // fn是调用者,被window所拥有
Dans l'exemple simple ci-dessus, fn() est un appelant indépendant. Selon la définition, son pointeur interne this n'est pas défini. Dans window.fn(), comme fn appartient à window, l'interne this pointe vers l'objet window.
Maintenant que vous maîtrisez cette règle, revenez maintenant en arrière et regardez les trois exemples ci-dessus en ajoutant/supprimant le mode strict, vous constaterez que cela est devenu moins illusoire et a des traces.
Mais ce à quoi nous devons prêter une attention particulière est demo03. Dans demo03, l'attribut c dans l'objet obj est calculé à l'aide de this.a 20, et son appelant obj.c n'est pas une fonction. Par conséquent, elle ne s’applique pas aux règles ci-dessus et nous devons tirer une conclusion distincte pour cette méthode.
Lorsque obj est déclaré globalement, peu importe où obj.c est appelé, cela pointe ici vers l'objet global, et lorsque obj est déclaré dans un environnement de fonction, cela pointe vers undéfini, en mode non strict , se tournera automatiquement vers l'objet global. Vous pouvez exécuter l'exemple ci-dessous pour voir la différence.
'use strict'; var a = 20; function foo () { var a = 1; var obj = { a: 10, c: this.a + 20, fn: function () { return this.a; } } return obj.c; } console.log(foo()); // 运行会报错
En développement réel, il n'est pas recommandé de l'utiliser de cette manière
上面多次提到的严格模式,需要大家认真对待,因为在实际开发中,现在基本已经全部采用严格模式了,而最新的ES6,也是默认支持严格模式。
再来看一些容易理解错误的例子,加深一下对调用者与是否独立运行的理解。
var a = 20; var foo = { a: 10, getA: function () { return this.a; } } console.log(foo.getA()); // 10 var test = foo.getA; console.log(test()); // 20
foo.getA()中,getA是调用者,他不是独立调用,被对象foo所拥有,因此它的this指向了foo。而test()作为调用者,尽管他与foo.getA的引用相同,但是它是独立调用的,因此this指向undefined,在非严格模式,自动转向全局window。
稍微修改一下代码,大家自行理解。
var a = 20; function getA() { return this.a; } var foo = { a: 10, getA: getA } console.log(foo.getA()); // 10
灵机一动,再来一个。如下例子。
function foo() { console.log(this.a) } function active(fn) { fn(); // 真实调用者,为独立调用 } var a = 20; var obj = { a: 10, getA: foo } active(obj.getA);
三、使用call,apply显示指定this
JavaScript内部提供了一种机制,让我们可以自行手动设置this的指向。它们就是call与apply。所有的函数都具有着两个方法。它们除了参数略有不同,其功能完全一样。它们的第一个参数都为this将要指向的对象。
如下例子所示。fn并非属于对象obj的方法,但是通过call,我们将fn内部的this绑定为obj,因此就可以使用this.a访问obj的a属性了。这就是call/apply的用法。
function fn() { console.log(this.a); } var obj = { a: 20 } fn.call(obj);
而call与applay后面的参数,都是向将要执行的函数传递参数。其中call以一个一个的形式传递,apply以数组的形式传递。这是他们唯一的不同。
function fn(num1, num2) { console.log(this.a + num1 + num2); } var obj = { a: 20 } fn.call(obj, 100, 10); // 130 fn.apply(obj, [20, 10]); // 50
因为call/apply的存在,这让JavaScript变得十分灵活。因此就让call/apply拥有了很多有用处的场景。简单总结几点,也欢迎大家补充。
1.将类数组对象转换为数组
function exam(a, b, c, d, e) { // 先看看函数的自带属性 arguments 什么是样子的 console.log(arguments); // 使用call/apply将arguments转换为数组, 返回结果为数组,arguments自身不会改变 var arg = [].slice.call(arguments); console.log(arg); } exam(2, 8, 9, 10, 3); // result: // { '0': 2, '1': 8, '2': 9, '3': 10, '4': 3 } // [ 2, 8, 9, 10, 3 ] // // 也常常使用该方法将DOM中的nodelist转换为数组 // [].slice.call( document.getElementsByTagName('li') );
2.根据自己的需要灵活修改this指向
var foo = { name: 'joker', showName: function() { console.log(this.name); } } var bar = { name: 'rose' } foo.showName.call(bar);
3.实现继承
// 定义父级的构造函数 var Person = function(name, age) { this.name = name; this.age = age; this.gender = ['man', 'woman']; } // 定义子类的构造函数 var Student = function(name, age, high) { // use call Person.call(this, name, age); this.high = high; } Student.prototype.message = function() { console.log('name:'+this.name+', age:'+this.age+', high:'+this.high+', gender:'+this.gender[0]+';'); } new Student('xiaom', 12, '150cm').message(); // result // ---------- // name:xiaom, age:12, high:150cm, gender:man;
简单给有面向对象基础的朋友解释一下。在Student的构造函数中,借助call方法,将父级的构造函数执行了一次,相当于将Person中的代码,在Sudent中复制了一份,其中的this指向为从Student中new出来的实例对象。call方法保证了this的指向正确,因此就相当于实现了基层。Student的构造函数等同于下。
var Student = function(name, age, high) { this.name = name; this.age = age; this.gender = ['man', 'woman']; // Person.call(this, name, age); 这一句话,相当于上面三句话,因此实现了继承 this.high = high; }
4.在向其他执行上下文的传递中,确保this的指向保持不变
如下面的例子中,我们期待的是getA被obj调用时,this指向obj,但是由于匿名函数的存在导致了this指向的丢失,在这个匿名函数中this指向了全局,因此我们需要想一些办法找回正确的this指向。
var obj = { a: 20, getA: function() { setTimeout(function() { console.log(this.a) }, 1000) } } obj.getA();
常规的解决办法很简单,就是使用一个变量,将this的引用保存起来。我们常常会用到这方法,但是我们也要借助上面讲到过的知识,来判断this是否在传递中被修改了,如果没有被修改,就没有必要这样使用了。
var obj = { a: 20, getA: function() { var self = this; setTimeout(function() { console.log(self.a) }, 1000) } }
另外就是借助闭包与apply方法,封装一个bind方法。
function bind(fn, obj) { return function() { return fn.apply(obj, arguments); } } var obj = { a: 20, getA: function() { setTimeout(bind(function() { console.log(this.a) }, this), 1000) } } obj.getA();
当然,也可以使用ES5中已经自带的bind方法。它与我上面封装的bind方法是一样的效果。
var obj = { a: 20, getA: function() { setTimeout(function() { console.log(this.a) }.bind(this), 1000) } }
四、构造函数与原型方法上的this
在封装对象的时候,我们几乎都会用到this,但是,只有少数人搞明白了在这个过程中的this指向,就算我们理解了原型,也不一定理解了this。所以这一部分,我认为将会为这篇文章最重要最核心的部分。理解了这里,将会对你学习JS面向对象产生巨大的帮助。
结合下面的例子,我在例子抛出几个问题大家思考一下。
function Person(name, age) { // 这里的this指向了谁? this.name = name; this.age = age; } Person.prototype.getName = function() { // 这里的this又指向了谁? return this.name; } // 上面的2个this,是同一个吗,他们是否指向了原型对象? var p1 = new Person('Nick', 20); p1.getName();
我们已经知道,this,是在函数调用过程中确定,因此,搞明白new的过程中到底发生了什么就变得十分重要。
通过new操作符调用构造函数,会经历以下4个阶段。
1.创建一个新的对象;
2.将构造函数的this指向这个新对象;
3.指向构造函数的代码,为这个对象添加属性,方法等;
4.返回新对象。
因此,当new操作符调用构造函数时,this其实指向的是这个新创建的对象,最后又将新的对方返回出来,被实例对象p1接收。因此,我们可以说,这个时候,构造函数的this,指向了新的实例对象,p1。
而原型方法上的this就好理解多了,根据上边对函数中this的定义,p1.getName()中的getName为调用者,他被p1所拥有,因此getName中的this,也是指向了p1。
好啦,我所知道的,关于this的一切,已经总结完了,希望大家在阅读之后,能够真正学到东西,然后给我点个赞^_^。如果你发现有什么错误,请在评论中指出,我会尽快修改。先谢过了。