this指向
- 普通函数:
this
的指向由调用方式决定,可以是全局对象、调用该函数的对象,或者显式指定的对象。- 箭头函数:
this
的指向在定义时确定,始终继承自外层函数作用域的this
,不会被调用方式影响。
var obj = {print: function() {var test = () => {console.log("yupi", this);}test();},rap: {doRap:() => {console.log(this);}}
}
var copyPrint = obj.print;
copyPrint();
obj.print();
obj.rap.doRap();//直接在 obj.rap 对象中定义,并没有包裹在任何外层函数中,会继承全局作用域的 this
var obj = {name: "yupi",func: function() {var self = this;console.log(this.name);console.log(self.name);(function() {console.log(this.name);console.log(self.name);}());}
};
obj.func();
//输出
yupi
yupi
undefined
yupi
var num = 2;
function print() {console.log(this.num);
}var obj = {num: 4,test1: print,test2: function () {print();},
};
obj.test1();
obj.test2();
var foo = obj.test1;
foo();
//输出结果
4
2
2
function test(num){this.value = num;return this;
}
var value = test(5);
var obj = test(6);console.log(value.value);
console.log(obj.value);
//输出结果
**undefined**6
function test(value) {this.num = value;
}var obj1 = {test: test,
};
obj1.test(1);
console.log(obj1.num);var obj2 = {};
obj1.test.call(obj2, 2);
console.log(obj2.num);var foo = new obj1.test(3);
console.log(obj1.num);
console.log(foo.num);//输出结果
1213function test(value){this.num = value;
}var obj1 = {};
var testBind = test.bind(obj1);
testBind(1);
console.log(obj1.num);var foo = new testBind(2);
console.log(obj1.num);
console.log(foo.num);
//输出结果
1
1
2
优先级:new 绑定>显示绑定( apply/call/bind )>
隐式绑定( obj.foo())>默认绑定( 独立函数调用 )
立即执行函数表达式 IIFE
什么是 IIFE(立即执行函数表达式)?
IIFE(Immediately Invoked Function Expression)是一种 JavaScript 编程模式,它使一个函数在定义时立即执行。IIFE 的格式通常如下所示:
(function() { // 函数体 })();
(() => { // 函数体 })();
立即执行函数的执行机制
- 在上面的代码中,IIFE 是通过
(function() {...})()
形式定义的。它是一个 函数表达式,并且在定义后立即执行。也就是说,IIFE 在 初始化时 执行一次,并且其作用域内的代码也只会执行一次。
对应到您的代码中的 IIFE
在您提供的代码中,IIFE 部分是:
window.num = 2;
var obj = {num: 4,test: (function(){console.log(this);this.num *= 6;return function(){console.log(this);this.num *= 8;}})()
}
var test = obj.test;
test();
obj.test();
console.log(obj.num);
console.log(window.num);//输出结果
Window 对象
Window 对象
{num: 4, test: ƒ} // obj 对象
32
96
- 这段代码中的 IIFE 会在 定义
test
属性时立即执行。console.log(this)
在 IIFE 执行时打印的是 全局对象window
,然后this.num *= 6;
会修改window.num
的值。 - 该 IIFE 返回了一个匿名函数,这个匿名函数会赋值给
obj.test
。
总结:IIFE 是立即执行的,因此它的代码块只会执行一次,并且返回的函数会被赋给 obj.test
。这个返回的函数之后会被调用,但 IIFE 本身只在定义时执行一次。
解释执行顺序
-
在执行到
var obj = { ... }
这一行时,IIFE 被立即执行:console.log(this)
打印window
对象。this.num *= 6
修改了window.num
(2 * 6 = 12
)。- 然后,IIFE 返回一个匿名函数,该匿名函数被赋给
obj.test
。
-
之后,
obj.test
就是 IIFE 返回的匿名函数,该函数在后面的调用中会执行:- 第一次调用
test()
时,this
指向window
,window.num
被修改为96
。 - 第二次调用
obj.test()
时,this
指向obj
,obj.num
被修改为32
。
- 第一次调用
IIFE 的核心特性:
- 只执行一次:IIFE 在定义时立即执行,并且它的作用域只会被创建一次。
- 返回值:IIFE 可以返回任何值,在您的代码中,它返回了一个函数,这个函数被赋值给
obj.test
。 - 默认this指向windows(非严格模式)
⭐⭐进阶题
var length = 10;
function func() {console.log(this.length);
}var obj = {length: 5,test: function(func) {console.log(this.length)//5func();//相当于window.func(),被window对象调用this指向window对象//func.call(this)//这样修改this指向,this就是指向objarguments[0]();//类似于func(),但是调用方式不同,this不同,含义大不相同,这个可以看作函数被arguments对象调用,而arguments对象本省就有length属性,即为参数的个数,所以this.length输出为2(参数个数)}
};
obj.test(func, 1);
输出为
- 10
- 2
刚看到一道JS面试题,关于this指向的问题
var length = 10;
function func() {
console.log(this.length);
}var obj = {
length: 5,
test: function(func) {
console.log(this.length)//5
func();//相当于window.func(),被window对象调用this指向window对象
//func.call(this)//修改this指向,this就是指向obj
arguments[0]();//类似于func(),但是调用方式不同,this不同,含义大不相同,这个可以看作函数被arguments对象调用,而arguments对象本身就有length属性,即为参数的个数,所以this.length输出为2(参数个数)
}
};
obj.test(func, 1);
⭐扩展知识:词法作用域
在 JavaScript 中,**词法作用域(Lexical Scope)**是指函数的作用域(即变量的可访问范围)是根据函数定义的位置来确定的,而不是根据函数调用的位置来决定的。这意味着 JavaScript 中的作用域链是静态的,它依赖于代码在编写时的结构。
1. 什么是词法作用域?
词法作用域(也叫静态作用域)是指一个函数在定义时决定了它能访问哪些变量。即,函数访问外部变量的规则与它们的定义位置有关,而与函数如何被调用时的调用位置无关。
- **函数的作用域链**是由函数定义时外部的作用域链决定的,而不是调用时的执行环境。
2. 如何理解词法作用域?
- 在 JavaScript 中,**变量的作用域**决定了变量的可访问范围。
- **函数的作用域**决定了函数内部可以访问哪些变量。
当你定义一个函数时,它会记住外部的作用域链,并且无论何时调用它,函数总是能访问到它定义时可访问的那些变量。
举个简单的例子:
function outer() {var outerVar = 'I am from outer!';function inner() {console.log(outerVar); // 访问外部函数的变量}inner(); // 调用 inner 函数
}outer(); // 输出 "I am from outer!"
在这个例子中:
- `inner` 函数定义在 `outer` 函数内部,因此它可以访问 `outer` 函数中定义的变量 `outerVar`。
- 这就是**词法作用域**,因为 `inner` 函数的作用域是由它定义的位置(即在 `outer` 函数内部)决定的,而不是由它调用的位置决定的。
3. 词法作用域与 `this`
在 JavaScript 中,`this` 的指向和作用域是两个不同的概念。虽然 `this` 和作用域链都与上下文相关,但它们的行为方式不同。理解词法作用域有助于理解 **箭头函数** 中 `this` 的行为。
普通函数和箭头函数的区别
- **普通函数**中的 `this` 是根据**调用时的上下文**来确定的。
- **箭头函数**中的 `this` 是在**定义时继承**外层作用域的 `this`,这就是箭头函数不同于普通函数的一大特点。
4. 箭头函数中的 `this`
箭头函数中的 `this` 绑定规则与普通函数不同。普通函数的 `this` 是动态绑定的,而箭头函数的 `this` 是静态的,指向定义时外层的 `this`。
例子:
function Outer() {this.name = 'Outer';// 普通函数this.printName = function() {console.log(this.name); // this 指向调用它的对象};// 箭头函数this.arrowPrintName = () => {console.log(this.name); // this 会继承自 Outer 函数的 this};
}var obj = new Outer();
obj.printName(); // 输出 'Outer',this 指向 obj
obj.arrowPrintName(); // 输出 'Outer',this 仍然指向 Outer 函数的 this
在上面的例子中:
- `printName` 是普通函数,它的 `this` 会根据调用时的上下文来确定。
- `arrowPrintName` 是箭头函数,它的 `this` 会继承外层 `Outer` 函数中的 `this`,即指向创建它时的 `this`,而不受 `obj` 的影响。
5. 作用域链
每个 JavaScript 函数都会创建一个作用域链,用于查找变量。当一个函数内部访问变量时,JavaScript 会沿着作用域链查找这个变量。如果函数内部没有定义某个变量,JavaScript 会查找函数外部的作用域,直到全局作用域为止。
- **作用域链**是由多个作用域组成的,最里层的是当前函数的作用域,外层是它的外部作用域,一直到全局作用域。```javascript
function outer() {var outerVar = 'outer';function inner() {var innerVar = 'inner';console.log(outerVar); // 在内层函数中访问外层函数的变量}inner();
}outer(); // 输出 'outer'
在上面的例子中:
- `inner` 函数在 `outer` 函数内部定义,它访问了外层的 `outerVar`。这是因为 `inner` 函数的作用域链包含了 `outer` 函数的作用域。
6. 词法作用域和闭包
https://gisjing.blog.csdn.net/article/details/143593869?spm=1001.2014.3001.5502&ydreferer=aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzU1MDQ5NjU1P3R5cGU9YmxvZw%3D%3D
闭包是指函数能够"记住"并访问它定义时的作用域,即使这个函数在定义时的作用域已经执行完毕。`
function outer() {var outerVar = 'outer';function inner() {console.log(outerVar); // 访问 outer 的变量}return inner;
}var closure = outer(); // 返回 inner 函数
closure(); // 输出 'outer'
在这个例子中,`inner` 函数是一个闭包,因为它记住了定义时的作用域(即 `outer` 函数的作用域),即使在外部调用时,`outerVar` 仍然是可以访问的。
7. 词法作用域与变量提升
JavaScript 中的**变量提升**(hoisting)指的是在执行代码之前,所有的 `var` 声明会被提升到作用域的顶部,但它们的赋值并不会被提升。```javascript
function example() {console.log(myVar); // undefined,因为声明被提升了,但赋值在后面var myVar = 10;
}example();
在这个例子中,`myVar` 的声明被提升了,但是它的值 `10` 是在执行到那行代码时才赋值的。所以输出是 `undefined`,而不是 `10`。
♥8. 总结
- **词法作用域**决定了变量和函数的可访问范围,它是由函数的定义位置而不是调用位置来确定的。
- **作用域链**:函数查找变量时,会沿着作用域链从内到外查找,直到全局作用域。
- **箭头函数的 `this`**:箭头函数的 `this` 不是根据调用方式决定的,而是继承自它定义时的外层作用域。
- **闭包**:函数可以记住并访问它定义时的作用域,即使外层函数已经执行完毕。
原型链
Javascript原型链-CSDN博客
- boy.prototype->undefined
- null.__proto__->TypeError
⭐⭐⭐JS中new关键字实例化对象,具体做了什么⭐⭐⭐
创建一个空对象:
- 当
new
关键字被调用时,JavaScript引擎首先会创建一个空白的对象。这个对象没有任何属性和方法,但它会继承自构造函数的prototype
属性所向的原型对指象。设置原型链:
- 接下来,JavaScript引擎会将新创建的对象的内部原型(
__proto__
)设置为构造函数的prototype
属性。这一步是原型链继承的关键,它允许新对象访问构造函数原型中定义的方法和属性。绑定
this
并执行构造函数:
- 然后,JavaScript引擎会将构造函数作为普通函数调用,但此时
this
关键字的值会被设置为新创建的对象。这意味着在构造函数内部,你可以通过this
来引用新对象,并向其添加属性和方法。- 构造函数中的代码会执行,可能会初始化对象的属性、调用其他方法等。
返回新对象:
- 最后,如果构造函数没有显式地返回一个对象(即返回的不是一个非原始值),那么
new
表达式会默认返回新创建的对象。如果构造函数返回了一个对象,那么new
表达式会返回这个对象,而不是新创建的那个空对象。需要注意的是,这种情况下,原型链的链接会失去作用,因为返回的对象与构造函数的原型没有直接联系。
- 创建一个新的空对象,并将这个对象的原型设置为函数的
prototype
属性。- 绑定函数内部的
this
到这个新对象,即在函数内this
指向新创建的对象。- 执行函数,将函数内部的代码与新对象的作用域绑定。
- 返回值处理:如果函数返回一个非
null
的对象,则new
表达式会返回这个对象;否则,它会返回创建的对象。
实现继承的几种方式
1.原型链继承
子类prototype指向父类实例。缺:所有实例共享父类属性,修改子类实例会影响到所有实例
function Coder() {this.type = 'Coder';
}Coder.prototype.rap = function () {console.log('yo yo yo');
};function Yupi(name) {this.name = name;this.age = 18;
}// 原型链继承
Yupi.prototype = new Coder();
Yupi.prototype.constructor = Yupi;// 测试
const yupi = new Yupi('Yupi');
console.log(yupi.type); // 输出: Coder
yupi.rap(); // 输出: yo yo yo
2.构造函数
子类构造函数调用父类构造函数实现,可继承父类属性,但不能继承父类原型方法
function Coder() {this.type = 'Coder';
}Coder.prototype.rap = function () {console.log('yo yo yo');
};function Yupi(name) {Coder.call(this); // 调用父类构造函数this.name = name;this.age = 18;
}// 测试
const yupi = new Yupi('Yupi');
console.log(yupi.type); // 输出: Coder
// yupi.rap(); // 这行代码会报错,因为构造函数继承无法继承原型链上的方法
3.组合继承(组合1和2)
开销较大,两次调用父类构造函数
function Coder() {this.type = 'Coder';
}Coder.prototype.rap = function () {console.log('yo yo yo');
};function Yupi(name) {Coder.call(this); // 调用父类构造函数this.name = name;this.age = 18;
}// 组合继承
Yupi.prototype = new Coder();
Yupi.prototype.constructor = Yupi;// 测试
const yupi = new Yupi('Yupi');
console.log(yupi.type); // 输出: Coder
yupi.rap(); // 输出: yo yo yo
4.寄生组合(推荐⭐)
避免组合继承两次调用父类构造函数
function Coder() {this.type = 'Coder';
}Coder.prototype.rap = function () {console.log('yo yo yo');
};function Yupi(name) {Coder.call(this); // 调用父类构造函数this.name = name;this.age = 18;
}// 寄生组合继承
Yupi.prototype = Object.create(Coder.prototype);
Yupi.prototype.constructor = Yupi;// 测试
const yupi = new Yupi('Yupi');
console.log(yupi.type); // 输出: Coder
yupi.rap(); // 输出: yo yo yo
5.ES6 class语法
class Coder {constructor() {this.type = 'Coder';}rap() {console.log('yo yo yo');}
}class Yupi extends Coder {constructor(name) {super(); // 调用父类构造函数this.name = name;this.age = 18;}
}// 测试
const yupi = new Yupi('Yupi');
console.log(yupi.type); // 输出: Coder
yupi.rap(); // 输出: yo yo yo
**基础补充**
function Father(){this.a=11;this.b=[1,2,this.a];this.print=function(){console.log(this.a,this.b,typeof(this.b[2]));}
}
const father=new Father();
father.print();
father.a=13;
father.print();
//输出
//11 [1,2,11] number
//13 [1,2,11] number
//b中的this.a是复制a的值,类型为原始类型
自己身上没有才去原型链上找
function Obj1() {}
function Obj2(value) {this.value = value;
}
function Obj3(value) {if (value) {this.value = value;}
}Obj1.prototype.value = 1;
Obj2.prototype.value = 1;
Obj3.prototype.value = 1;console.log(new Obj1().value);
console.log(new Obj2().value);
console.log(new Obj3(666).value);1
undefined
666
函数原型与对象原型
var FuncObj = function () {};
Object.prototype.foo = function () {console.log('foo');
};
Function.prototype.bar = function () {console.log('bar');
};
FuncObj.foo();
FuncObj.bar();var f = new FuncObj();
f.foo();
f.bar();//输出结果
foo
bar
foo
TypeError: f.bar is not a function
⭐⭐**进阶题1**⭐⭐
前端分享一经典有难度易错的Javascript题目
function Father() {this.a = 1;this.b = [1, 2, this.a];this.c = { field: 5 };this.print = function () {console.log(this.a, this.b, this.c.field);};
}function Son() {this.a = 2;this.update = function () {this.b.push(this.a);this.a = this.b.length;this.c.field = this.a++;};
}Son.prototype = new Father();
var father = new Father();
var son1 = new Son();
var son2 = new Son();
son1.a = 11;
son2.a = 12;
father.print();
son1.print();
son2.print();
son1.update();
son2.update();
father.print();
son1.print();
son2.print();
1 [ 1, 2, 1 ] 5
11 [ 1, 2, 1 ] 5
12 [ 1, 2, 1 ] 5
1 [ 1, 2, 1 ] 5
5 [ 1, 2, 1, 11, 12 ] 5
6 [ 1, 2, 1, 11, 12 ] 5
代码执行过程及结果分析
-
定义
Father
和Son
构造函数:Father
构造函数初始化属性a
(值为1),b
(值为[1, 2, this.a]
,即[1, 2, 1]
),c
(对象{ field: 5 }
),以及print
方法。Son
构造函数初始化属性a
(值为2)和update
方法。Son.prototype = new Father();
使得Son
的实例对象能够继承Father
中定义的a
、b
、c
和print
。
-
实例化对象:
var father = new Father();
创建了一个Father
的实例father
,其属性值为a = 1
,b = [1, 2, 1]
,c = { field: 5 }
。var son1 = new Son();
和var son2 = new Son();
创建了两个Son
的实例son1
和son2
。由于继承了Father
,它们各自拥有独立的a
值(由Son
构造函数初始化为2)和独立的b
、c
的引用(通过继承Father
的实例化对象)。因此,son1
和son2
的初始属性为a = 2
,b = [1, 2, 1]
,c = { field: 5 }
。
-
修改属性:
son1.a = 11;
将son1
的a
属性设置为11。son2.a = 12;
将son2
的a
属性设置为12。
-
调用
print
方法:father.print();
输出father
的属性:a = 1
,b = [1, 2, 1]
,c.field = 5
。结果为:1 [1, 2, 1] 5
son1.print();
输出son1
的属性:a = 11
,b = [1, 2, 1]
,c.field = 5
。结果为:11 [1, 2, 1] 5
son2.print();
输出son2
的属性:a = 12
,b = [1, 2, 1]
,c.field = 5
。结果为:12 [1, 2, 1] 5
-
调用
update
方法:-
son1.update();
执行以下操作:this.b.push(this.a);
将son1.a
(即11)添加到b
中,因此son1.b
变为[1, 2, 1, 11]
。this.a = this.b.length;
将son1.a
更新为b
的长度(4)。this.c.field = this.a++;
将son1.c.field
设置为a
(即4),然后自增a
为5。- 更新后,
son1.a = 5
,son1.b = [1, 2, 1, 11]
,son1.c = { field: 4 }
。
-
son2.update();
执行以下操作:this.b.push(this.a);
将son2.a
(即12)添加到b
中,因此son2.b
变为[1, 2, 1, 11, 12]
。this.a = this.b.length;
将son2.a
更新为b
的长度(5)。this.c.field = this.a++;
将son2.c.field
设置为a
(即5),然后自增a
为6。- 更新后,
son2.a = 6
,son2.b = [1, 2, 1, 11, 12]
,son2.c = { field: 5 }
。
-
-
再次调用
print
方法:father.print();
仍然输出原始的father
属性:a = 1
,b = [1, 2, 1]
,c.field = 5
。结果为:1 [1, 2, 1] 5
son1.print();
输出更新后的son1
属性:a = 5
,b = [1, 2, 1, 11, 12]
,c.field = 5
。结果为:5 [1, 2, 1, 11, 12] 5
son2.print();
输出更新后的son2
属性:a = 6
,b = [1, 2, 1, 11, 12]
,c.field = 5
。结果为:6 [1, 2, 1, 11, 12] 5
在这个示例中,son1
和 son2
实例共享了 Father
原型中的 b
和 c
引用,因此 update
方法对 b
数组的修改会影响到所有 Son
实例。
⭐⭐**进阶题2**⭐⭐
function FuncObj() {print = function () {console.log(1);};//重新给print变量赋值return this;//返回了this
}FuncObj.print = function () {console.log(2);
};FuncObj.prototype.print = function () {console.log(3);
};var print = function () {console.log(4);
};function print() {console.log(5);
}FuncObj.print();
print();
FuncObj().print();//相当于调用windows.print()
print();
new FuncObj.print();//调用静态方法FuncObj.print()
new FuncObj().print();//构造方法FuncObj()实例化对象,对象的print()没找到->原型上找
new new FuncObj().print();//构造方法FuncObj()实例化对象找原型上print(),再调用此方法-》仍输出3
//输出结果
2
4(变量提升,var与函数声明同名时,var声明会在提升阶段覆盖函数声明)
1
1
2
3
3
调用和输出分析
1. FuncObj.print();
- 这是调用
FuncObj
的静态方法print
。 - 输出:
2
2. print();
- 此时
print
是定义为var print = function () { console.log(4); }
。 - 输出:
4
3. FuncObj().print();
- 调用
FuncObj()
,由于FuncObj
返回this
(即全局对象window
),它会将print
重新定义为function () { console.log(1); }
,覆盖了全局的print
。 - 然后调用
print()
,即function () { console.log(1); }
。 - 输出:
1
4. print();
- 上一步中
print
被重新赋值为function () { console.log(1); }
。 - 输出:
1
5. new FuncObj.print();
new FuncObj.print()
调用的是FuncObj
的静态方法print
,而非实例化FuncObj
。new
操作符在此上下文中无意义,但会正常执行。【简单来讲就是不涉及原型链和this使用】- 输出:
2
为什么
new
在这里无意义在这个过程中,
new
仅仅用来调用FuncObj.print
,并没有创建FuncObj
的实例。它的主要效果仅是创建一个与FuncObj
无关的空对象,作为FuncObj.print
的this
,并执行了FuncObj.print
函数体。
new
在此无意义,因为FuncObj.print
并不使用this
,返回的空对象也没有用。- 效果等同于
FuncObj.print()
,唯一的区别是返回一个空对象,但对FuncObj.print
的执行没有任何影响。
6. new FuncObj().print();
new FuncObj()
创建了一个FuncObj
的实例,该实例继承了FuncObj.prototype.print
方法。- 调用实例的
print
方法,即FuncObj.prototype.print
。 - 输出:
3
7. new new FuncObj().print();
new FuncObj()
创建了一个FuncObj
的实例,且new FuncObj().print
返回FuncObj.prototype.print
方法。- 最外层
new
调用FuncObj.prototype.print
方法,仍然输出3
。 - 输出:
3