this理解
1.全局代码眼中的this
- 全局范围内的this会指向全局对象,浏览器中的window
alert(this); // window
2.函数代码中的this(默认绑定)
- this指向全局对象,即window,如果声明了严格模式,则是undefined
function foo(a) {
this.a = a;
}
foo(2);
alert(x); // 全局变量2
// 严格模式下
var a = 1
function fn() {
var a = 2
console.log(this.a) // console what ?
}
fn();// undefined
3.对象中的函数调用this
- this指向person对象,即当前对象
var name = ‘hello’;
var person = {
name: 'word',
test: function(a) {
var saytest = function(a) {
console.log(this.name+ 'nihao' + a);
};
saytest(a);
}
}
person.test('hello, word');// word nihao hello word
4.隐式绑定(属性访问调用)
1. 如果函数调用时,前面存在调用它的对象,那么this就会隐式绑定到这个对象上,调用堆栈的上一级
// 第一种情况
function fn () {
console.log(this.a)
}
const obj = {
a: 1
}
obj.fn = fn
obj.fn() // 1
// 第二种情况
function fn () {
console.log(this.a)
}
const obj1 = {
a: 1,
fn
}
const obj2 = {
a: 2,
obj1
}
obj2.obj1.fn() // 1
2.隐式绑定失效例子
// 第一种 是前面提过的情况
const obj1 = {
a: 1,
fn: function() {
console.log(this.a)
}
}
const fn1 = obj1.fn // 将引用给了 fn1,等同于写了 function fn1() { console.log(this.a) }
fn1() // 所以这里其实已经变成了默认绑定规则了,该函数 `fn1` 执行的环境就是全局环境
// 第二种 setTimeout
setTimeout(obj1.fn, 1000) // 这里执行的环境同样是全局
// 第三种 函数作为参数传递
function run(fn) {
fn()
}
run(obj1.fn) // 这里传进去的是一个引用
// 第四种 一般匿名函数也是会指向全局的
var name = 'The Window';
var obj = {
name: 'My obj',
getName: function() {
return function() { // 这是一个匿名函数
console.log(this.name)
};
}
}
obj.getName()()
// 第五种 函数赋值也会改变 this 指向
// 第六种 IIFE
5.this显式绑定(call,bind,apply)
1.通过显式绑定强行绑定this上下文
let obj1 = {
name: '你在说什么'
};
let obj2 = {
name: '我不能说吗'
};
let obj3 = {
name: 'echo'
}
var name = '哈哈哈哈';
function fn() {
console.log(this.name);
};
fn(); //哈哈哈哈
fn.call(obj1); //你在说什么
fn.apply(obj2); //我不能说吗
fn.bind(obj3)(); //echo
6.new
- 实现一个new函数
// new 关键字会进行如下操作
// 1. 创建一个空的javascript对象
// 2. 链接该对象到令一个对象,如果函数 constructor 里没有返回对象的话,this 指向的是 new 之后得到的实例
// 3. 将新创建的对象作为this上下文
// 4. 如果改函数没又返回对象,这返回this
function mynew() {
if(typeof fn !== 'function') throw new Error('fn must be a function.');
mynew.tatget = fn;
const temp = Object.create(fn.prototype);
const res = fn.apply(temp,...args);
retunrn _.isObject(res) ? res : temp;
}
7.箭头函数
- 箭头函数情况比较特殊,编译期间确定的上下文,不会被改变,哪怕你new,指向的就是上一层的上下文,this本身是没有this的,继承的是最外层的
function fn() {
return {
b: () => {
console.log(this);
}
}
}
fn().b() // window 全局对象
fn().b.bind(1)() // window 全局对象
fn.bind(2)().b.bind(3)() // 2
call、apply与bind有什么区别?
1.call、apply与bind都用于改变this绑定,但call、apply在改变this指向的同时还会执行函数,而bind在改变this后是返回一个全新的boundFcuntion绑定函数,这也是为什么上方例子中bind后还加了一对括号 ()的原因。
2.bind属于硬绑定,返回的 boundFunction 的 this 指向无法再次通过bind、apply或 call 修改;call与apply的绑定只适用当前调用,调用完就没了,下次要用还得再次绑。
3.call与apply功能完全相同,唯一不同的是call方法传递函数调用形参是以散列形式,而apply方法的形参是一个数组。在传参的情况下,call的性能要高于apply,因为apply在执行时还要多一步解析数组
8.优先级
// 隐式 vs 默认 -> 结论:隐式 > 默认
function fn() {
console.log(this)
}
const obj = {
fn
}
obj.fn() // fn
// 显式 vs 隐式 -> 结论:显式 > 隐式
obj.fn.bind(5)() // 5
// new vs 显式 -> 结论:new > 显式
function foo (a) {
this.a = a
}
const obj1 = {}
var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a) // 2
// new
var baz = new bar(3)
console.log( obj1.a ) // 2
console.log( baz.a ) // 3
// 所以得出结论 「new 绑」 > 「显绑」 > 「隐绑」 > 「默认绑定」
作用域
执行上下文,主要分为
1. 变量对象(VO)/活动对象(AO)👆
var a = '1';
function sum() {
if(a=== '1') {
a = 2;
}else {
a = 3;
}
}
sum();
// 函数sum的作用域链包含了2个对象,一个是自己的变量对象,另一个是全局上下文的变量对象
//局部作用域中定义的变量可以用于局部上下文中替换全局变量
var test = ‘hello’;
function changeTest() {
let antoTest = ‘hello1’;
funciton swapTest() {
let tempTest = antoTest;
antoTest = test;
test = tempTest;
// 可以访问到color,antoTest、tempTest;
}
swapTest(); // 可以访问到test和antoTest
}
changeTest(); //只能访问到test
2.作用域链👆
- 每一个执行上下文都与一个作用域链相关联。作用域链是一个对象组成的链表,求值标识符的时候会搜索它。当控制进入执行上下文时,就根据代码类型创建一个作用域链,并用初始化对象(VO/AO)填充。执行一个上下文的时候,其作用域链只会被 with 声明和 catch 语句所影响
var a = 20;
function foo(){
var b = 100;
alert( a + b );
}
foo();
// 两个阶段:创建 - 执行
// --------------------------- 创建 ------------------------------
// 模拟 VO/AO 对象
AO(foo) {
b: void 0
}
// [[scope]] 不是作用域链,只是函数的一个属性(规范里的,不是实际实现)
// 在函数创建时被存储,静态(不变的),永远永远,直到函数被销毁
foo.[[scope]]: [VO(global)]
VO(global) {
a: void 0,
foo: Reference<'foo'>
}
// --------------------------- 调用 ------------------------------
// 可以这么去理解,近似的用一个 concant 模拟,就是将当前的活动对象放作用域链最前边
Scope = [AO|VO].concat([[Scope]])
// ---------------------------- 执行时 EC --------------------------------
EC(global) {
VO(global) {
a: void 0,
foo: Reference<'foo'>
},
Scope: [VO(global)],
// this
}
EC(foo) {
AO(foo) { // 声明的变量,参数
b: void 0
},
Scope: [AO(foo), VO(global)] // 查找顺序 -> RHS LHS
}
3. 变量声明 👆
1.使用var的函数作用域声明
在var声明变量的时候,变量会被自动添加到上下文,在函数中醉接近的上下文就是函数的局部上下文
function sum(num1,num2) {
var sum = num1 + num2;
return sum;
}
let res = sum(1,2); // 3
console.log(sum);报错: sum在这里不是有效的变量
var name = ‘hello’;
//等价于
name = ‘hello’;
var name;
// 在声明之前打印变量,可以验证变量会提升
console.log(name); // undefined
var name = 'hello';
function() {
console.log(name); // undefined
var name = 'hello';
}
2.使用let的会计作用域声明
- let 关键字和var相似,但是它的作用域是块级的,if块,while块、function块
// let
if(true){
let a;
}
console.log(a); // ReferenceError: a没有定义
while (true) {
let b;
}
console.log(b); // ReferenceError: a没有定义
function foo() {
let c;
}
console.log(c); // ReferenceError: a没有定义
// var
var a;
var a; // 不会报错
{
let b;
let b;
}
// SyntaxError: 标识符b 已经声明过了
总结: 1.let 的行为非常适合在循环引用中声明迭代变量,使用var 声明的迭代变量会泄漏到循环外部, 2. let 在js运行中也会被提升,但是由于暂时性死区的缘故,实际上不能在声明之前使用let变量
for(var i=0;i<10;i++) {}
console.log(i);// 10
for(var j=0;j<10;j++) {}
console.log(j);// j没有定义
3.使用const的常量声明
- 使用const 声明的变量必须同时初始化伟某个值.一旦被声明,都不能在重新赋值;
// const
if(true){
const a = 1;
}
console.log(a); // ReferenceError: a没有定义
while (true) {
const b = 1;
}
console.log(b); // ReferenceError: a没有定义
function foo() {
const c = 1;
}
console.log(c); // ReferenceError: a没有定义
- const 声明只能应用到顶级的原语或者对象;另一个意思说,赋值为对象的const 变量不能在被重新赋值为其他引用值,当对象的键泽不受限制;
const obj = {};
obj = {}; // TypeError:给常量赋值
const obj2 ={};
obj2.name ='hello';
console.log(obj2.name); // hello
// 让整个对象不能修改,可以使用Object.freeze(),这样在给属性赋值时候虽然不会报错,但会静默失败
```js
const obj = Object.freeze({});
obj.name = 'hello';
console.log(obj.name); // undefined