JavaScript 高级程序设计
原型链与面向对象编程
this 指向、闭包和作用域
Promise规范及应用
手写JS 常见 API
本文档使用 MrDoc 发布
-
+
首页
this 指向、闭包和作用域
## this 指向 ### this 指针概念 可以认为 this 是当前函数或当前模块的运行环境的上下文,是一个指针型变量,可以理解为动态的对象,普通函数的 this 是在调用时才被绑定确认指向的。 this 是动态的,那么如何判断它到底绑定的是什么内容? ### 默认绑定 指函数独立调用的时候,不带任何修饰的函数引用 * 非严格模式下,this 指向全局对象(浏览器下指向 Window,Node.js 环境指向 Global) * 严格模式下,this 绑定到 undefined,严格模式下不允许 this 指向全局对象 抛出错误,Uncaught TypeError: Cannot read properties of undefined (reading ‘a’) ```js var a = 'hello' var obj = { a: 'tom', foo: function () { // 'use strict'; console.log(this.a) // if use strict > Uncaught TypeError: // Cannot read property 'a' of undefined }, } var bar = obj.foo bar() // hello ``` 注意 Tips:普通函数作为参数传递的情况,比如 setTimeout,setInterval, 在非严格模式下的 this 指向全局对象 ```js var name = 'tom' const sayHi = () => { console.log(this) // { name: 'hahahha', sayHi: Fn} setTimeout(function () { console.log('Hello,', this.name) // Hello, tom }) // 此时如果setTimeout中的函数是箭头函数则,取上文this指向 setTimeout(() => { console.log('Hello,', this.name) // Hello, hahahha }) } var person = { name: 'hahahha', sayHi: sayHi, } person.sayHi() ``` ### 隐式绑定 与默认绑定相反,函数调用的时候,有显示的修饰,比如说某个对象的函数。 如下: ```js var a = 'hello' var obj = { a: 'tom', foo: function () { console.log(this.a) }, } obj.foo() // tom ``` 疑问? Tips: 那如果有链式调用的情况,this 会绑定到哪个对象上? 答案是就近的对象 ```js function sayHi() { console.log('Hello,', this.name) } var person2 = { name: 'tom', sayHi: sayHi, } var person1 = { name: 'hahaha', friend: person2, } person1.friend.sayHi() // Hello, tom ``` ### 显示绑定 通过 call apply bind 可以修改 this 的指向(call、apply、bind 都挂在自 Function 原型下) call 和 apply 的异同 * call 和 apply 的第一个参数会绑定到函数体的 this 上。如果不传参,非严格模式下,this 默认会被绑定到全局对象上 -call 函数接收的是一个参数列表,apply 函数接收的是一个参数数组 ```js func.call(this, arg1, arg2, ...); func.call(this, [arg1, arg2, ...]); ``` 注意 Tips:如果我们调用 call 和 apply 时,传入的是基本类型数字或字符串,绑定 this 的时候会把他们转换成对象 ```js function getThisType() { console.log('this指向内容', this, typeof this) } getThisType.call(1) // this指向内容 Number {1} object getThisType.apply('lubai') // this指向内容 String {'lubai'} object ``` bind bind 方法会创建一个新的函数。第一个参数依旧将作为它运行时的 this, 之后传入的一系列参数将会在传递的实参前传入作为它的参数。 ```js func.bind(thisArg, args1, args2, ...); ``` 如下 ```js var publicAccounts = { name: 'hahah', author: 'tom', subscribe: function (subscriber) { console.log(`${subscriber}${this.name}`) }, } publicAccounts.subscribe('jack') // jack hahah var subscribe1 = publicAccounts.subscribe.bind( { name: '测试名称A', author: '测试作者B' }, '测试订阅者C' ) subscribe1() // 测试订阅者C 测试名称A ``` ### new 绑定 构造函数中的 this 指向了新生成的实例。 ```js function study(name) { this.name = name } var studyDay = new study('tom') console.log(studyDay) // { name: 'tom' } console.log(studyDay.name) // tom ``` ### this 绑定的优先级 [new]绑定 > [call、apply、bind]显示绑定 > [fn.play('a')]隐式绑定 > [play('a')]默认绑定 ### 箭头函数 特征: 1. 箭头函数没有 arguments,如果发现在箭头函数中使用了 arguments, 那么真正拿到的是外层 function 函数的传参 2. 如果要拿到箭头函数的所有参数,我们可以直接用参数结构的方式 ```js let nums = (...nums) => nums ``` 3. 箭头函数没有构造函数,所以不能用 new 来调用 ```js let fun = () => {} let funNew = new fun() // Uncaught TypeError: fun is not a constructor ``` 4. 箭头函数没有原型对象,fun.prototype undefined 5. 箭头函数没有自己的 this,它的 this 指向由该函数被定义的位置决定 ```js var name = 'tom' var person = { name: 'hahahahahah', sayHi: sayHi, } function sayHi() { console.log(this) // { name: hahahahhah, sayHi: Fn } setTimeout(() => { console.log('Hello,', this.name) // Hello, hahahahhah }) } person.sayHi() ``` ### 练习 练习 1 ```js var name = '123' var obj = { name: '456', print: function () { function a() { console.log(this.name) } a() }, } obj.print() // 123 ``` 练习 2 ```js function Foo() { Foo.a = function () { console.log(1) } this.a = function () { console.log(2) } } Foo.prototype.a = function () { console.log(3) } Foo.a = function () { console.log(4) } Foo.a() // 4 let obj = new Foo() obj.a() // 2 Foo.a() // 1 ``` ### [#](http://xiaozhouboom.com/js/01-js.html#_9-%E6%89%8B%E5%86%99%E5%AE%9E%E7%8E%B0-bind)9. 手写实现 bind ```js Function.prototype.bind = function (context, ...args) { context = context || window const fnSymbol = Symbol('fn') context[fnSymbol] = this // 由于需要返回一个新函数 return function (..._args) { _args = args.concat(_args) context[fnSymbol](..._args) delete context[fnSymbol] } } const obj = { name: 'zg', sex: '女', } function test(name, sex) { this.name = name this.sex = sex console.log(this) } console.log(obj) // { name: 'zg', sex: '女'} const fn = test.bind(obj, 'tom') fn('男') console.log(obj) // { name: 'tom', sex: '男'} ``` ### 手写实现 call ```js Function.prototype.call = function (context, ...args) { context = context || window const fnSymbol = Symbol('fn') context[fnSymbol] = this const result = context[fnSymbol](...args) delete context[fnSymbol] return result } const obj = { name: 'zg', sex: '女', } function test(name, sex) { this.name = name this.sex = sex } console.log(obj) // { name: 'zg', sex: '女'} test.call(obj, 'tom', '男') console.log(obj) // { name: 'tom', sex: '男'} ``` ### [#](http://xiaozhouboom.com/js/01-js.html#_11-%E6%89%8B%E5%86%99%E5%AE%9E%E7%8E%B0-apply)11. 手写实现 apply ```js Function.prototype.apply = function (context, argsArr) { context = context || window const fnSymbol = Symbol('fn') context[fnSymbol] = this const result = context[fnSymbol](...argsArr) delete context[fnSymbol] return result } const obj = { name: 'zg', sex: '女', } function test(name, sex) { this.name = name this.sex = sex } console.log(obj) // { name: 'zg', sex: '女'} test.apply(obj, ['tom', '男']) console.log(obj) // { name: 'tom', sex: '男'} ``` ### 手写实现 new ```js function mockNew(Constructor, ...args) { const newObj = Object.create(Constructor.prototype) const res = Constructor.apply(newObj, args) return typeof res === 'object' ? res : newObj } ``` ## 闭包 ### 闭包概念 闭包是指能够访问自由变量的函数。 自由变量是指,既不是函数参数,也不是函数局部变量的变量。 高级程序设计:闭包指有权访问另一个函数作用域的函数,可以理解为那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现。 ### 闭包应用 ### 柯里化函数 柯理化的目的是:避免调用具有相同参数的函数,同时又能够轻松复用。其实就是封装一个高阶函数 ```js // 假设我们要求一个长方形的面积 function area(width, height) { return width * height } // 并且我们遇到的三个长方形宽都是10,那么 const area1 = area(10, 20) const area2 = area(10, 30) const area3 = area(10, 40) // 柯里化后 function getArea(width) { return (height) => { return width * height } } const getTenWitdhArea = Area(10) // 此时遇到宽度不变的情况下就能轻松复用了 const getTwentyArea = getTenWitdhArea(20) // 遇到宽度偶尔变化时,也可以 const getTwentyWitdhArea = Area(20) ``` ### 使用闭包实现私有方法和变量 其实就是模块的方式,现代化的打包最终其实就是每个模块的代码都是相互独立的。 ```js function funOne(number) { var name = 'tom' // 私有变量 function funTwo() { // 私有方法 console.log('数字:', number) console.log('姓名:', name) } return funTwo } const f1 = funOne(110) const f2 = funOne(120) ``` ### 匿名自执行函数 ```js var funOne = (function () { var num = 0 return function () { num++ return num } })() console.log(funOne()) // 输出:1 console.log(funOne()) // 输出:2 console.log(funOne()) // 输出:3 ``` ### 缓存结果 比如外部函数创建一个数组,闭包函数内可修改和获取数组的值,其实还是延长了变量的生命周期,但不通过全局变量来实现。 ```js function funParent() { let memo = [] return (i) => { memo.push(i) console.log(memo.join(',')) } } const fn = funParent() fn(1) fn(2) ``` ### 总结 * 可创建私有变量 * 延长变量的生命周期 一般函数的词法环境在函数返回后就销毁。但是闭包会保存对创建时所在词法环境的引用,即便创建时所在执行上下文已被销毁,但创建的词法环境依然存在,以达到延长变量的声明周期的目的 ### 面试题 实现 compose 函数, 得到如下输出 ```js 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 // 实现compose函数 function compose() { const arr = Array.from(arguments) // 转为数组的方式 // const argFnList = [...arguments]; // 转为数组的方式 return (num) => { return arr.reduce((pre, cur) => cur(pre), num) } } // function compose(...args) { // 解构的方式 // return (num) => { // return args.reduce((pre,cur) => cur(pre), num); // } // } ``` 实现函数柯里化 ```js function currying(fn, ...args) { const originFnArgumentLength = fn.length let allArgs = [...args] // 不传参实现 // const fn = arguments[0]; // const originFnArgumentLength = fn.length; // let _args = Array.from(arguments).slice(1); const resFn = (...newArgs) => { allArgs = [...allArgs, ...newArgs] if (allArgs.length === originFnArgumentLength) { return fn(...allArgs) } else { return resFn } } return resFn } const add = (a, b, c) => a + b + c const a1 = currying(add, 1) const a2 = a1(2) console.log(a2(3)) // 6 ``` ## 作用域 ### 作用域概念 作用域是在运行时,代码中的某些特定部分中变量、函数和对象的可访问性。 换句话说,作用域决定了代码区块中变量和其他资源的可见性。 作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下的同名变量不会有冲突。 ES6 之前 Javascript 没有块级作用域,只有全局作用域和函数作用域。ES6 的到来,为我们提供了 **块级作用域** ,可通过新增 let 和 const 来体现。 ### 全局作用域 在代码中任何地方都能访问到的对象拥有全局作用域。 * 最外层函数和在最外层函数外面定义的变量拥有全局作用域 ```js 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 ``` * 所有未定义直接赋值的变量自动声明为拥有全局作用域 ```js function outFun2() { variable = '未定义直接复制的变量' var inVariable2 = '内层变量' } outFun2() console.log(variable) // "未定义直接复制的变量" console.log(inVariable2) // inVariable2 is not defined ``` * 所有 window 对象的属性拥有全局作用域 window.location * 弊端 如果我们写了很多 JS 代码,变量定义都没有用函数包裹,那么它们就全部在全局作用域中,这样就会污染全局命名空间,容易引起命名冲突。 ### 函数作用域 函数作用域,是指声明在函数内部的变量,和全局作用域相反,局部作用域一般只固定在代码片段内可访问到,最常见的例如函数内部。 ```js function doSomething() { var blogName = '浪里行舟' function innerSay() { alert(blogName) } innerSay() } alert(blogName) // blogName is not defined innerSay() // innerSay is not defined ``` 作用域是分层的,内层作用域可以访问外层作用域的变量,反之则不行 ### 块级作用域 块级作用域可以通过新增命令 let 和 const 声明,所声明的变量在制定块的作用域外无法被访问。块级作用域在如下情况被创建: * 在一个函数内部 * 在一个代码块(由一对花括号包裹)内部 let 声明的语法与 var 的语法一致。你基本上可以用 let 来代替 var 进行变量声明,但会将变量的作用域限制在代码块中。 块级作用域有以下几个特点: * 声明变量不会提升到代码块顶部 * 禁止重复声明 * 变量只在当前块内有效 ### 作用域面试题 ```js for (var i = 0; i < 10; i++) { setTimeout(function () { console.log(i) }) } // 输出 10 10 10 ... 10 for (let i = 0; i < 10; i++) { setTimeout(function () { console.log(i) }) } // 输出 0 1 2 3 4 5 6 7 8 9 ``` 第一个变量 i 为 var 声明的,在全局范围内有效,所有全局中只有一个变量 i,每次循环时,setTimeout 定时器里指的是全局变量 i,而由于事件循环机制,setTimeout 是在循环结束后才执行,所以输出十个 10。 第二个变量 i 是用 let 声明的,仅在本轮循环中有效,每次循环的 i 实际上都是一个新的变量,所以定时器里面的 i 其实不是同一个变量,所以输出 0 到 9 ### 作用域链 作用域链类似原型链,当在找一个变量的时候,如果当前作用域找不到,那就会逐级往上去查找,直到找到全局作用域还是没找到,那就确实找不到了。 注意 Tips:最先的作用域是创建这个函数的域。作用域中的取值,这里强调的是“创建”,而不是调用 ```js var a = 10 function fn() { var b = 20 function bar() { console.log(a + b) // 30 } return bar } var x = fn(), b = 200 x() // bar() ``` ### 作用域链面试题 ```js var b = 10 ;(function c() { b = 20 // 在内部作用域中,会先去查找是否有变量b的声明,有就直接赋值20。但此处发现了具名函数function b(){}, 拿此b做赋值; // IIFE的函数无法进行赋值(内部机制,类似const定义的常量),所以无效。 console.log(b) // fn b console.log(window.b) // 10 })() ``` * 函数表达式与函数声明不同,函数名只在该函数内部有效,并且此绑定是常量绑定。 * 对于一个常量进行赋值,在 严格模式下会报错,非严格模式下静默失败。 * IIFE 中的函数是函数表达式,而不是函数声明。 注意 Tips: IIFE 中的函数是函数表达式,会用函数名定义变量,不可被修改! ```js var a = 3 function c() { // 最先的作用域是创建这个函数的域 此处 a = 3 alert(a) } ;(function () { var a = 4 c() // 3 })() ``` ### 变量提升、函数提升 ```js function v() { var a = 6 function a() {} console.log(a) } v() // 6 ``` ```js function v() { var a function a() {} console.log(a) } v() // fn a ``` 由上示例可得: js 会把所有变量的定义依次集中提升到作用域顶部事先声明好,但是它赋值的时机是依赖代码的位置。也就是变量会事先声明,但是变量不会事先赋值。 函数声明提升的规则是在变量声明的后面。 ### 变量提升、函数提升面试题 ```js 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() ```
追风者
2022年3月3日 23:00
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
关于 MrDoc
觅思文档MrDoc
是
州的先生
开发并开源的在线文档系统,其适合作为个人和小型团队的云笔记、文档和知识库管理工具。
如果觅思文档给你或你的团队带来了帮助,欢迎对作者进行一些打赏捐助,这将有力支持作者持续投入精力更新和维护觅思文档,感谢你的捐助!
>>>捐助鸣谢列表
微信
支付宝
QQ
PayPal
Markdown文件
分享
链接
类型
密码
更新密码