前言
在上篇中,我们总结了 JavaScript
这门语言的基础知识,而这篇则是讲述这门语言的特色,也是它的核心知识、面试重点。
函数
函数可谓是JavaScript中的一等公民,与函数涉及的也有相当多的概念,当初笔者刚学的时候被绕的云里雾里,下面先以一个问题,开始函数的总结与学习。
怎么执行一段JavaScript代码
JavaScript
代码的执行主要分为以下三步:
- 分析有没有词法、语法错误
- 预编译-发生在函数执行的前一刻,生成变量环境、词法环境
- 解释执行
而 JavaScript
比较有特色的就是在预编译阶段,我们重点看看预编译阶段做了什么事情。
预编译
引擎一开始会创建执行上下文(也叫Activation Object
、AO
对象),执行上下文主要有如下三种类型:
- 全局执行上下文:只有一个
- 函数执行上下文:存在无数个,每个函数被调用就新建一个
Eval
执行上下文:eval
中运行的函数代码,很少用
执行上下文的创建主要分为创建阶段和执行阶段
创建阶段
1.绑定 this
指向
2.创建词法环境
3.生成变量环境
这里解释一下词法环境和变量环境,其实他们两个是差不多相同的组件。词法环境中包含两个部分,一个是存储变量与函数声明的位置,另一个是对外部环境的引用。
伪代码如下:
GlobalExectionContext = { // 全局执行上下文 LexicalEnvironment: { // 词法环境 EnvironmentRecord: { // 环境记录 Type: "Object", // 全局环境 // 标识符绑定在这里 outer: <null>, // 对外部环境的引用 } } } FunctionExectionContext = { // 函数执行上下文 LexicalEnvironment: { // 词法环境 EnvironmentRecord: { // 环境记录 Type: "Declarative", // 函数环境 // 标识符绑定在这里 // 对外部环境的引用 outer: <Global or outer function environment reference> } }
变量环境也是一个词法环境,词法环境和变量环境的区别在于:
- 词法环境存储函数声明和绑定
let
和const
变量 - 变量环境仅绑定
var
变量
执行阶段
完成对所有变量的分配,最后执行代码
作用域&作用域链
每个 JavaScript
函数都是一个对象,对象中有的属性可以访问,有的不能,这些属性仅供 JavaScript
引擎存取,如 [[scope]]
。
[[scope]]就是函数的作用域,其中存储了执行上下文的集合。
[[scope]]
中所存储的执行上下文对象的集合,这个集合呈链式链接,我们称这种链式链接为作用域链。查找变量时,要从作用域链的顶部开始查找。在当前执行上下文中找不到变量时,则到对外部环境的引用中向上查找,故呈现一个链式结构。
作用域与变量声明提升
- 在
JavaScript
中,函数声明与变量声明会被JavaScript
引擎隐式地提升到当前作用域的顶部 - 声明语句中的赋值部分并不会被提升,只有名称被提升
- 函数声明的优先级高于变量,如果变量名跟函数名相同且未赋值,则函数声明会覆盖变量声明
- 如果函数有多个同名参数,那么最后一个参数(即使没有定义)会覆盖前面的同名参数
闭包
当内部函数被保存到外部时,将会生成闭包。生成闭包后,内部函数依旧可以访问其所在的外部函数的变量。
当函数执行时,会创建执行上下文,获取作用域链(存储了函数能够访问的所有执行上下文)。函数每次执行时对应的执行上下文都是独一无二的,当函数执行完毕,函数都会失去对这个作用域链的引用, JS
的垃圾回收机制是采用引用计数策略,如果一块内存不再被引用了那么这块内存就会被释放。
但是,当闭包存在时,即内部函数保留了对外部变量的引用时,这个作用域链就不会被销毁,此时内部函数依旧可以访问其所在的外部函数的变量,这就是闭包。
即闭包逃过了 GC
策略,故滥用会导致内存泄漏,其实本身就是一种内存泄漏?
经典题目
for (var i = 0; i < 5; i++) { setTimeout(function timer() { console.log(i) }, i * 100) }
function test() { var a = []; for (var i = 0; i < 5; i++) { a[i] = function () { console.log(i); } } return a; } var myArr = test(); for(var j=0;j<5;j++) { myArr[j](); }
以上两个例子都打印5个5,简单解释就是变量 i
记录的是最终跳出循环的值,即5,可以通过立即执行函数或者 let
来解决。因为立即执行函数创建了一个新的执行上下文,可以保存当前循环 i
的值,而let则构建了块级作用域,也可以保存当前循环 i
的值。
for (var i = 0; i < 5; i++) { ;(function(i) { setTimeout(function timer() { console.log(i) }, i * 100) })(i) }
function test(){ var arr=[]; for(i=0;i<10;i++) { (function(j){ arr[j]=function(){ console.log(j); } })(i) } return arr; } var myArr=test(); for(j=0;j<10;j++) { myArr[j](); }
封装私有变量
function Counter() { let count = 0; this.plus = function () { return ++count; } this.minus = function () { return --count; } this.getCount = function () { return count; } } const counter = new Counter(); counter.puls(); counter.puls(); console.log(counter.getCount())
计数器
实现一个foo函数 可以这么使用:
a = foo(); b = foo(); c = foo(); // a === 1;b === 2;c === 3; foo.clear();d = foo(); //d === 1;
function myIndex() { var index = 1; function foo(){ return index++; } foo.clear = function() { index = 1; } return foo; } var foo = myIndex();
JavaScript 中,调用函数的方式?
在 JavaScript
中,调用函数的方式主要有如下数种
- 方法调用模式
Foo.foo(arg1, arg2)
; - 函数调用模式
foo(arg1, arg2)
; - 构造器调用模式
(new Foo())(arg1, arg2)
; call
/apply
调用模式Foo.foo.call(that, arg1, arg2)
;bind
调用模式Foo.foo.bind(that)(arg1, arg2)()
;
防抖节流
无论是面试还是业务开发,这都是经常接触到的知识,我们一起来看看
防抖 debounce
函数防抖就是在函数需要频繁触发的情况下,只有足够的空闲时间,才执行一次。
典型应用
- 百度搜索框在输入稍有停顿时才更新推荐热词。
- 拖拽
function debounce(handler, delay = 300){ var timer = null; return function(){ var _self = this, _args = arguments; clearTimeout(timer); timer = setTimeout(function(){ handler.apply(_self, _args); }, delay); }
// 频繁触发时,清除对应的定时器,然后再开一个定时器,delay秒后执行 function debounce(handler, delay){ delay = delay || 300; var timer = null; return function(){ var _self = this, _args = arguments; clearTimeout(timer); timer = setTimeout(function(){ handler.apply(_self, _args); }, delay); } } // 不希望被频繁调用的函数 function add(counterName) { console.log(counterName + ": " + this.index ++); } // 需要的上下文对象 let counter = { index: 0 } // 防抖的自增函数,绑定上下文对象counter let db_add = debounce(add, 10).bind(counter) // 每隔500ms频繁调用3次自增函数,但因为防抖的存在,这3次内只调用一次 setInterval(function() { db_add("someCounter1"); db_add("someCounter2"); db_add("someCounter3"); }, 500) /** * 预期效果: * * 每隔500ms,输出一个自增的数 * 即打印: someCounter3: 0 someCounter3: 1 someCounter3: 2 someCounter3: 3 */
节流 throttle
一个函数只有在大于执行周期时才执行,周期内调用不执行。好像水滴积攒到一定程度才会触发一次下落一样。
典型应用:
- 抢券时疯狂点击,既要限制次数,又要保证先点先发出请求
- 窗口调整
- 页面滚动
function throttle(fn,wait=300){ var lastTime = 0 return function(){ var that = this,args=arguments var nowTime = new Date().getTime() if((nowTime-lastTime)>wait){ fn.apply(that,args) lastTime = nowTime } } }
this
在上面说函数的时候,我们也提到了一下 this
,即函数创建执行上下文的时候第一步就是绑定 this
的指向,也对应了那句话-- JavaScript
中 this
的指向是当函数执行的时候才确定的。
this
的指向主要有如下数种:
- 作为函数直接调用,非严格模式下,
this
指向window
,严格模式下,this
指向undefined
- 作为某对象的方法调用,
this
通常指向调用的对象 - 使用
apply
、call
、bind
可以绑定this
指向 - 在构造函数中,
this
指向新创建的对象 - 箭头函数没有单独的
this
值,this
在箭头创建时绑定,它与声明所在的上下文相同
当多个this出现时,this改指向哪里?
首先, new
的方式优先级最高,接下来是 bind
这些函数,然后是 obj.foo()
这种调用方式,最后是 foo
这种调用方式,同时,箭头函数的 this
一旦被绑定,就不会再被任何方式所改变。
new
绑定- 显式绑定->
bind
、apply
、call
- 隐式绑定->
obj.foo()
- 默认绑定-> 浏览器环境默认是
window
默认绑定
function foo() { // 运行在严格模式下,this会绑定到undefined "use strict"; console.log( this.a ); } var a = 2; // 调用 foo(); // TypeError: Cannot read property 'a' of undefined // -------------------------------------- function foo() { // 运行 console.log( this.a ); } var a = 2; foo()//2
隐式绑定
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; obj.foo(); // 2
注意下面这种情况,称为隐式丢失。
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; var bar = obj.foo; // 函数别名 var a = "global"; // a是全局对象的属性 bar(); // "global"
显示绑定
function foo() { console.log( this.a ); } var obj = { a: 2 }; foo.call( obj ); // 2 调用foo时强制把foo的this绑定到obj上
new 绑定
function foo(a) { this.a = a; } var bar = new foo(2); // bar和foo(..)调用中的this进行绑定 console.log( bar.a ); // 2
某厂面试
请分别写出下面题目的答案。
function Foo() { getName = function() { console.log(1); }; return this; } Foo.getName = function() { console.log(2); }; Foo.prototype.getName = function() { console.log(3); }; var getName = function() { console.log(4); }; function getName() { console.log(5); } //请写出以下输出结果: Foo.getName(); //-> 2 Foo对象上的getName() ,这里不会是3,因为只有Foo的实例对象才会是3,Foo上面是没有3的 getName(); //-> 4 window上的getName,console.log(5)的那个函数提升后,在console.log(4)的那里被重新赋值 Foo().getName(); //-> 1 在Foo函数中,getName是全局的getName,覆盖后输出 1 getName(); //-> 1 window中getName(); new Foo.getName(); //-> 2 Foo后面不带括号而直接 '.',那么点的优先级会比new的高,所以把 Foo.getName 作为构造函数 new Foo().getName();//-> 3 此时是Foo的实例,原型上会有输出3这个方法
箭头函数中的this判断
箭头函数里面的 this
是继承它作用域父级的 this
, 即声明箭头函数处的 this
let a = { b: function() { console.log(this) }, c: () => { console.log(this) } } a.b() // a a.c() // window let d = a.b d() // window
bind、apply实现
自封装 bind
方法
- 因为
bind
的使用方法是 某函数.bind(某对象,...剩余参数)
- 所以需要在Function.prototype 上进行编程
- 将传递的参数中的某对象和剩余参数使用
apply
的方式在一个回调函数中执行即可 - 要在第一层获取到被绑定函数的
this
,因为要拿到那个函数用apply
/** * 简单版本 */ Function.prototype.myBind = (that, ...args) => { const funcThis = this; return function(..._args) { return funcThis.apply(that, args.concat(_args)); } } Function.prototype.mybind = function(ctx) { var _this = this; var args = Array.prototype.slice.call(arguments, 1); return function() { return _this.apply(ctx, args.concat(args, Array.prototype.slice.call(arguments))) } }
/** * 自封装bind方法 * @param {对象} target [被绑定的this对象, 之后的arguments就是被绑定传入参数] * @return {[function]} [返回一个新函数,这个函数就是被绑定了this的新函数] */ Function.prototype.myBind = function (target){ target = target || window; var self = this; var args = [].slice.call(arguments, 1); var temp = function(){}; var F = function() { var _args = [].slice.call(arguments, 0); return self.apply(this instanceof temp ? this: target, args.concat(_args)); } temp.prototype = this.prototype; //当函数是构造函数时,维护原型关系 F.prototype = new temp(); return F; }
自封装一个apply
- 首先要先原型上即
Function.prototype
上编程 - 需要拿到函数的引用, 在这里是
this
- 让 传入对象.fn =
this
- 执行 传入对象.fn(传入参数)
- 返回执行结果
Function.prototype.myApply = function(context) { if (typeof this !== 'function') { throw new TypeError('Error') } context = context || window context.fn = this let result // 处理参数和 call 有区别 if (arguments[1]) { result = context.fn(...arguments[1]) } else { result = context.fn() } delete context.fn return result }
new实现
new
的过程
- 新生成一个对象
- 链接到原型
- 绑定
this
- 返回新对象
function create() { let obj = {} obj.__proto__ = con.prototype con.call(this) return obj };
JavaScript核心知识总结(中)二