this 指针详解
概念
this 是当前函数/当前模块运行环境的上下文,是一个指针变量,普通函数中的this 是在调用时才被绑定确认指向的。
通过不同的 this 调用同一个函数,可以产出不同的结果
到底如何确认 this 绑定的内容
this 的绑定规则
1. 默认绑定
直接调用函数,不使用点操作符调用
非严格模式下指向全局对象,浏览器环境是 window,node 环境是 global
严格模式下绑定到 undefined。
Tips: 普通函数做为参数传递的情况, 比如setTimeout, setInterval, 非严格模式下的this指向全局对象
2. 隐式绑定
某个对象通过点运算符调用函数。this 指向该对象。
3. 显示绑定
通过 bind、call、apply 把函数绑定到指定的变量上。
- bind 展开传参,只绑定不调用,返回一个新函数
- call 展开传参,绑定的同时执行函数
- apply 传入参数列表,绑定的同时执行函数
function person (name, sex) {
console.log(this, name, sex)
}
const obj = {
name: 'ss',
sex: 'M'
}
person.call(obj, 'ss', 'M')
person.apply(obj, ['ss', 'M'])
const b = person.bind(obj, 'ss')
b()
Tips:如果我们传入 call/apply/bind 的第一个参数是一个数字、布尔值、string 等基本数据类型,绑定时会装箱成一个对象
手写 bind
Function.prototype.bind2 = function (context) {
context = context || window
const fnSymbol = Symbol('fn')
context[fnSymbol] = this
let args = Array.prototype.slice.call(arguments, 1)
return function () {
args = args.concat(Array.from(arguments))
context[fnSymbol](...args)
delete context[fnSymbol]
}
}
function foo (a, b) {
console.log(this, a, b)
}
const obj = {
name: 'ss',
sex: 'M'
}
const f2 = foo.bind2(obj, 'hahah')
f2('ss')
const f3 = foo.bind(obj, 'hahah')
f3('ss')
call
Function.prototype.call2 = function (context, ...args) {
context = Object(context || window)
const fnSymbol = Symbol('fn')
context[fnSymbol] = this
const result = context[fnSymbol](...args)
delete context[fnSymbol]
return result
}
function foo (a, b) {
console.log(this, a, b)
}
const obj = {
name: 'ss'
}
foo.call2(obj, 3, 5)
console.log(obj)
foo.call(obj, 3, 5)
4. new 绑定
new 的功能
- 创建一个空对象
- 将对象的proto 属性指向构造函数的 prototype
- 将构造函数的 this 指向该新建对象,执行构造函数
- 如果构造函数执行结果为对象,则返回该对象,否则返回之前新建的对象
构造函数中的 this 指向了新生成的实例对象 studyDay
function study (name) {
this.name = name
}
const studyDay = new study('ss')
console.log(studyDay) // { name: 'ss' }
console.log(studyDay.name) // ss
5. this 绑定的优先级
new > 显式 > 隐式 > 默认
箭头函数
- 箭头函数没有 arguments
- 箭头函数不能用作构造函数
- 箭头函数没有原型对象
let fun = () => {}
console.log(fun.prototype) // undefined
- 箭头函数没有自己的 this
闭包的概念及应用场景
闭包是能访问自由变量的函数
自由变量是指在函数中使用的,既不是函数局部变量也不是函数参数的变量
- 从理论角度:所有的函数都是闭包。因为他们都是在创建的时候就将上层上下文数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于在访问自由变量,这个时候使用最外层的作用域
- 从实践角度:以下函数才算是闭包:
- 即使创建他的上下文已经销毁,他仍然存在(比如内部函数从父函数中返回)
- 在代码中引用了自由变量
应用场景
- 柯里化的目的在于:避免频繁调用传相同参数的函数。同时又能够轻松复用。
其实就是封装一个高阶函数
function getArea(width, height) {
return width * height
}
function getWidthArea(width) {
return function (height) {
return width * height
}
}
const getArea2 = getWidthArea(2)
console.log(getArea2(6))
const getArea10 = getWidthArea(10)
console.log(getArea10(11))
- 使用闭包实现私有方法和变量
function Person () {
let _name = 'ss'
return {
getName() {
return _name
},
setName(name) {
_name = name
}
}
}
const person = Person()
person.setName('xy')
console.log(person.getName())
- 匿名自执行函数
const funcOne = (function(){
let i = 0
return () => {
i++
console.log(i)
}
})()
funcOne() // 1
funcOne() // 2
funcOne() // 3
- 缓存一些结果
比如在外部函数创建一个数组,闭包函数内可以更改/获取这个数组的值,其实还是延长变量的声明周期,但是不通过全局变量来实现。
function funParent () {
let memo = []
function funTwo (i) {
memo.push(i)
console.log(memo.join(,))
}
return funTwo
}
const fn = funParent()
fn(1)
fn(2)
总结
- 创建私有变量
- 延长变量的生命周期
一般函数的词法环境在函数返回后就被销毁了,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在词法环境依然存在,以达到延长变量的生命周期的目的
代码题
- 实现 compose 函数,得到如下输出
function fn1(x) {
return x + 1
}
function fn2(x) {
return x + 2
}
function fn3(x) {
return x + 3
}
function fn4(x) {
return x + 4
}
const a = compose(fn1, fn2, fn3, fn4);
console.log(a('1')); // 1+4+3+2+1=11
function compose (...fns) {
fns = fns.reverse()
return (x) => {
return fns.reduce((pre, cur) => cur(pre), x)
}
}
- 实现一个柯里化函数
function currying(fn, ...args) {
const fnArgLength = fn.length
const retFunc = (..._args) => {
args = args.concat(_args)
return args.length >= fnArgLength
? fn(...args)
: retFunc
}
return retFunc
}
const add = (a, b, c) => a + b + c;
const a1 = currying(add, 1, 2, 3, 4);
const a2 = a1();
console.log(a2) // 6
作用域
作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。
换句话说,作用域决定了代码区块中变量和其他资源的可见性
作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说,作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
ES6 之前 JavaScript 没有块级作用域,只有全局作用域和函数作用域。ES6 的到来,为我们提供了块级作用域,可通过let
和const
来定义块级作用域变量
全局作用域
在代码中任何地方都可以访问变量和函数拥有全局作用域
- 最外层函数 和 定义在最外层函数外面的变量 有全局作用域
var outVariable = "我是最外层变量"; //最外层变量
function outFun() { //最外层函数
var inVariable = "内层变量";
function innerFun() { //内层函数
console.log(inVariable);
}
innerFun();
}
console.log(outVariable); //我是最外层变量
outFun(); //内层变量
console.log(inVariable); //inVariable is not defined
innerFun(); //innerFun is not defined
- 所有未定义直接赋值的变量自动声明为拥有全局作用域的变量
function outFunc2() {
variable = '未定义直接复制的变量'
var inVariable2 = '内层变量2'
}
outFunc()
console.log(variable) // 未定义直接复制的变量
console.log(inVariable2); //inVariable2 is not defined
- 所有 window 对象的属性都有全局作用域
window.location
- 弊端
定义全局变量容易造成全局命名空间污染,引起变量冲突
函数作用域
函数作用域,是指声明在函数内部的变量,只有在函数内部才能访问到
作用域是分层的,内层作用域可以访问外层作用域的变量,反之不行
块级作用域
块级作用域可通过新增命令 let 和 const 声明,所声明的变量在指定块的作用域外无法被访问。块级作用域 在如下情况下被创建:
- 在一个函数内部
- 在一个代码块(由一对花括号包裹)内部
let 声明的语法与 var 的语法一致。你基本上可以用 let 来代替 var 进行变量声明,但会将变量的作用域限制在当前代码块中。
块级作用域有以下几个特点:
- 声明变量不会提升到代码块顶部
- 禁止重复声明
- 变量只在当前块内有效
作用域链
有点儿类似于原型链,在原型链上早一个属性的时候,如果当前实例找不到,就回去父级原型上去找。
作用域链也是类似的原理,找一个变量的时候,如果当前作用域找不到,就会逐级网上查找,找到找到全局作用域还是没有找到,就真的找不到了。
Tips: 函数内有效的作用域链值指的是创建函数时的作用域链,不是执行函数时的作用域链
const foo = (function() {
let a = 1
return function() {
console.log(a)
}
})();
(function(){
let a = 3
foo()
})()
// 1
const foo = (function() {
// let a = 1
return function() {
console.log(a)
}
})();
(function(){
let a = 3
foo()
})()
// a is not defined
const foo = (function() {
// let a = 1
return function() {
console.log(a)
}
})();
let a = 2;
(function(){
let a = 3
foo()
})()
// 2
Coding
- 看一下输出
var b = 10;
(function b(){
b = 20;
// 内部作用域,会先去查找是有已有变量b的声明,有就直接赋值20,确实有了呀。发现了具名函数 function b(){},拿此b做赋值;
// IIFE的函数无法进行赋值(内部机制,类似const定义的常量),所以无效。
console.log(b); // fn b
console.log(window.b); // 10
})();
- IIFE 中的函数是函数表达式不是函数声明
- 函数表达式与函数声明不同,函数名只在函数内部有效,并且此绑定是常量绑定
- 对一个常量赋值,在严格模式下报错,非严格模式静默失败
- 看一下输出
var a = 3;
function c() {
alert(a);
}
(function () {
var a = 4;
c(); // 3
})();
- 看一下输出
function v() {
var a = 6
function a () {
}
console.log(a) // 6
}
js会把所有变量都集中提升到作用域顶部事先声明好,但是它赋值的时机是依赖于代码的位置,那么js解析运行到那一行之后才会进行赋值,还没有运行到的就不会事先赋值。也就是变量会事先声明,但是变量不会事先赋值。
碰到这种问题可以先想一下变量提升和函数声明提升的规则, 原则上是变量被提升到最顶部, 函数声明被提升到最顶部变量的下方.
尝试着把这两段代码在大脑中编译一下:
- 第一段代码
function v() {
var a;
function a() {
}
a=6;
console.log(a);
}
v(); // 6
- 第二段代码
function v() {
var a;
function a() {
}
console.log(a);
}
v(); // fn a
- 看一下输出
function v() {
console.log(a); // fn a
var a = 1;
console.log(a); // 1
function a() {
}
console.log(a); // 1
console.log(b); // fn b
var b = 2;
console.log(b); // 2
function b() {
}
console.log(b); // 2
}
v();
按照刚才的思路转换一下:
function v() {
var a;
var b;
function a() {}
function b() {}
console.log(a); // fn a
a=1;
console.log(a); // 1
console.log(a); // 1
console.log(b); // fn b
b=2;
console.log(b); // 2
console.log(b); // 2
}
v();