一、关于this指向的几种场景
1、默认绑定(函数直接调用)
非严格模式下,默认绑定指向全局(
node
中是global
)
function fn() {
console.log(this)
}
fn()
function fn() {
'use strict'
console.log(this) // undefined
}
fn()
把最外层 var a = 1 -> let a = 1
var a = 1
function fn() {
var a = 2
console.log(this.a) // 1
}
fn()
let a = 1
function fn() {
var a = 2
console.log(this.a) // undefined
}
fn()
指向调用者
var b = 1
function outer () {
var b = 2
function inner () {
console.log(this.b) // 1
}
inner()
}
outer()
const obj = {
a: 1,
fn: function() {
console.log(this.a)
}
}
obj.fn() // 1
const f = obj.fn
f() // undefined
2、隐式绑定(属性访问调用)
隐式绑定的
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
隐式绑定失效指向哪里?
- 直接赋值,this指向改变
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()()
3、显式绑定(call
、 bind
、 apply
)
这种根本还是取决于第一个参数,但是第一个为
null
的时候还是绑到全局的
function fn () {
console.log(this.a)
}
const obj = {
a: 100
}
fn.call(obj)
bind 只看第一个 bind(堆栈的上下文,上一个,写的顺序来看就是第一个)
function fn() {
console.log(this) // 1
}
// 为啥可以绑定基本类型 ?
// boxing(装箱) -> (1 ----> Number(1))
fn.bind(1).bind(2)()
bind实现
// Yes, it does work with `new (funcA.bind(thisArg, args))`
if (!Function.prototype.bind) (function(){
var ArrayPrototypeSlice = Array.prototype.slice; // 为了 this
Function.prototype.bind = function(otherThis) {
// 调用者必须是函数,这里的 this 指向调用者:fn.bind(ctx, ...args) / fn
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
var baseArgs= ArrayPrototypeSlice.call(arguments, 1), // 取余下的参数
baseArgsLength = baseArgs.length,
fToBind = this, // 调用者
fNOP = function() {}, // 寄生组合集成需要一个中间函数,避免两次构造
fBound = function() {
// const newFn = fn.bind(ctx, 1); newFn(2) -> arguments: [1, 2]
baseArgs.length = baseArgsLength; // reset to default base arguments
baseArgs.push.apply(baseArgs, arguments); // 参数收集
return fToBind.apply( // apply 显示绑定 this
// 判断是不是 new 调用的情况,这里也说明了后边要讲的优先级问题
fNOP.prototype.isPrototypeOf(this) ? this : otherThis, baseArgs
);
};
// 下边是为了实现原型继承
if (this.prototype) { // 函数的原型指向其构造函数,构造函数的原型指向函数
// Function.prototype doesn't have a prototype property
fNOP.prototype = this.prototype; // 就是让中间函数的构造函数指向调用者的构造
}
fBound.prototype = new fNOP(); // 继承中间函数,其实这里也继承了调用者了
return fBound; // new fn()
};
})();
4、new的指向
实现new
// new 关键字会进行如下的操作:
// 1. 创建一个空的简单JavaScript对象(即{});
// 2. 链接该对象(设置该对象的constructor)到另一个对象 ;
// 3. 将步骤1新创建的对象作为this的上下文 ;// 🔥
// 4. 如果该函数没有返回对象,则返回this。
// 我们来模拟实现一个 new
// new Fn();
// myNew(Fn, ...args);
import _ from 'lodash';
function myNew(fn, ...args) {
// fn 必须是一个函数
if (typeof fn !== 'function') throw new Error('fn must be a function.')
// es6 new.target
myNew.target = fn
// 原型继承
const temp = Object.create(fn.prototype) // 步骤 1. 2.
// fn执行绑定 this 环境
const res = fn.apply(temp, ...args) // 步骤 3.
// 如果该函数没有返回对象,则返回this。
return _.isObject(res) ? res : temp
}
如果函数
constructor
里没有返回对象的话,this
指向的是new
之后得到的实例
function foo(a) {
this.a = a
}
const f = new foo(2)
f.a // 2
function bar(a) {
this.a = a
return {
a: 100
}
}
const b = new bar(3)
b.a // 100
5、箭头函数
箭头函数本身是没有
this
的,继承的是外层的
function fn() {
return {
b: () => {
console.log(this)
}
}
}
fn().b() // console what?
fn().b.bind(1)() // console what?
fn.bind(2)().b.bind(3)() // 2
6、优先级
「new 绑」 > 「显绑」 > 「隐绑」 > 「默认绑定」
// 隐式 vs 默认 -> 结论:隐式 > 默认
function fn() {
console.log(this)
}
const obj = {
fn
}
obj.fn() // what ?
// 显式 vs 隐式 -> 结论:显式 > 隐式
obj.fn.bind(5)() // what ?
// new vs 显式 -> 结论:new > 显式
function foo (a) {
this.a = a
}
const obj1 = {}
var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a) // what ?
var baz = new bar(3)
console.log( obj1.a ) // what ?
console.log( baz.a ) // what ?
// 箭头函数没有 this,比较没有意义
7、实战题
- 1、
function foo() {
console.log( this.a ) // 2
}
var a = 2;
(function(){
"use strict" // 迷惑大家的
foo();
})();
- 2、
var name="the window"
var object={
name:"My Object",
getName: function(){
return this.name
}
}
object.getName() // My Object
(object.getName)() // My Object
// 运算符导致丢失了this指向
(object.getName = object.getName)() // the window
(object.getName, object.getName)() // the window
- 3、
var x = 3
var obj3 = {
x: 1,
getX: function() {
var x = 5
return function() {
return this.x
}(); // ⚠️
}
}
console.log(obj3.getX()) // 3
- 4、
function a(x){
this.x = x
return this
}
var x = a(5) // 替换为 let 再试试
var y = a(6) // 替换为 let 再试试 // 再换回 var,但是去掉 y 的情况,再试试
console.log(x.x) // undefind
console.log(y.x) // 6
// 等价于
window.x = 5;
window.x = window;
window.x = 6;
window.y = window;
console.log(x.x) // void 0 其实执行的是 Number(6).x
console.log(y.x) // 6
二、作用域和作用域链
作用域
作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。在ES5中,变量的作用域有全局作用域和局部作用域,局部作用域又称为函数作用域。ES6引入了块级作用域,明确允许在块级作用域中声明函数。
全局作用域:
- 程序最外层定义的函数或者变量
- 末定义直接赋值的变量
- Windows对象的属性和方法
局部作用域:
- 在函数内创建,函数外不可访问
作用域链
当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain),其实就是对执行上下文EC中的变量对象VO|AO有序访问的链表。能按顺序访问到VO|AO。
function a() {
function b() {
var b = 234;
}
var a = 123;
b();
}
var gloab = 100;
a();
第一步:a定义
a 函数在被定义时,a函数对象的属性[[scope]]作用域指向他的作用域链scope chain,此时它的作用域链的第一项指向了GO(Global Object)全局对象,我们看到全局对象上此时有5个属性,分别是this、window、document、a、glob第二步:a执行、b定义
当a函数被执行时,b函数被定义,此时a函数对象的作用域[[scope]]的作用域链scope chain的第一项指向了AO(Activation Object)活动对象,AO对象里有4个属性,分别是this、arguments、a、b。
第二项指向了GO(Global Object),GO对象里依然有5个属性,分别是this、window、document、a、golb。第三步:b执行
当b函数被执行时,此时b函数对象的作用域[[scope]]的作用域链scope chain的第一项指向了AO(Activation Object)活动对象,AO对象里有3个属性,分别是this、arguments、b。
第二项指向了AO(Activation Object)活动对象,AO对象里有4个属性,分别是this、arguments、a、b。
第三项指向了GO(Global Object),GO对象里依然有5个属性,分别是this、window、document、a、golb。什么是执行上下文?
当控制器转到一段可执行代码的时候就会进入到一个执行上下文。执行上下文是一个栈结构(先进后出), 栈底部永远是全局上下文,栈顶是当前活动的上下文。其余都是在等待的状态。
当浏览器首次载入你的脚本,它将默认进入全局执行上下文。
如果,你在你的全局代码中调用一个函数,你程序的时序将进入被调用的函数,并穿件一个新的执行上下文,并将新创建的上下文压入执行栈的顶部。浏览器将总会执行栈顶的执行上下文,一旦当前上下文函数执行结束,它将被从栈顶弹出,并将上下文控制权交给当前的栈。
闭包
我们有时候需要得到函数内的局部变量,由于作用域的概念,在函数外部无法读取函数内的局部变量,但是在函数的内部,再定义一个函数,把变量作为返回值,我们就可以在函数外部读取它的内部变量了。
总之,闭包就是能够读取其他函数内部变量的函数,因此可以把闭包简单理解成"定义在一个函数内部的函数"。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
function f1(){
var n=999;
function f2(){
alert(n);
}
// f2函数,就是闭包
return f2;
}
var result=f1();
result(); // 999
作用
- 可以读取函数内部的变量
- 让这些变量的值始终保持在内存中
注意事项
以上例子,f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(为解决内存的泄露,垃圾回收机制会定期(周期性)找出那些不再用到的内存(变量),然后释放其内存)回收。
由于闭包会使得函数中的变量都保存在内存中,内存消耗很大,所以在退出函数之前,记得将不使用的局部变量全部删除,否则可能导致内存泄漏(不再用到的内存,没有及时释放,就叫做内存泄漏)。
应用场景
- getter和setter
function fn(){
var name='hello'
setName=function(n){
name = n;
}
getName=function(){
return name;
}
//将setName,getName作为对象的属性返回
return {
setName:setName,
getName:getName
}
}
var fn1 = fn();//返回对象,属性setName和getName是两个函数
console.log(fn1.getName());//getter
fn1.setName('world');//setter修改闭包里面的name
console.log(fn1.getName());//getter
- 迭代器(执行一次函数往下取一个值)
var arr =['aa','bb','cc'];
function incre(arr){
var i=0;
return function(){
//这个函数每次被执行都返回数组arr中 i下标对应的元素
return arr[i++] || '数组值已经遍历完';
}
}
var next = incre(arr);
console.log(next());//aa
console.log(next());//bb
console.log(next());//cc
console.log(next());//数组值已经遍历完
- setTimeout延时赋值
//每秒执行1次,分别输出1-10
for(var i=1;i<=10;i++){
(function(j){
//j来接收
setTimeout(function(){
console.log(j);
},j*1000);
})(i)//i作为实参传入
}