目录
1.函数概念,声明及调用
2.函数表达式(匿名函数)
3.函数传参
4.修改input的值
5.函数的不定参(可变参)—关键字arguments
6.函数返回值
7.封装获取元素的方法
8.获取计算后样式—getComputedStyle(el)
8.JS预解析机制(变量提升Hoisting)
9.作用域
10.window
11.全局污染(命名冲突问题)
12.作用域链(scope chain)
13.闭包
14.this当前执行代码的环境对象
15.严格模式下的this指向
16.this指向的修改
16.1 function.call()
16.2 function.apply()
16.3 function.bind()
16.4 bind方法原理分析:
函数的基本概念、声明及调用;函数作用域、作用域链、闭包;this指向及修改和绑定this指向等。
JS中的函数:把一段需要重复使用的代码,用function语法包起来,方便重复调用,分块和简化代码。复杂一点的,也会加入封装、抽象、分类等思想。
声明方式:严格意义上两种方式,但还有匿名函数
function 方法名(){
//要执行的代码
}
函数调用:两种方式调用
//函数声明
function fn(){
console.log(1);
}
//函数的调用
fn();
调用方式二:在事件中调用,直接写函数名,不使用括号
//函数声明
function fn(){
console.log(1);
}
//函数在事件中的调用
document.onclick = fn;
函数表达式:就是把函数存到变量里。
匿名函数:没有名字的函数;
匿名函数在使用时只有两种情况:
(function(){
console.log("匿名函数自执行");
})();
//2,函数表达式:把函数存到变量或数组等里,调用时通过变量进行调用
var fn = function(){
console.log("函数表达式:将函数存到变量里");
};
fn();//调用时需要写括号
//2,函数表达式:把函数存到数组第0位,调用时通过数组第0位进行调用
var arr = [];
arr[0] = function(){
console.log("函数表达式:将函数存到数组的对应位置");
};
arr[0]();//调用时需要写括号要写括号
结果:
事件函数扩展:给元素添加事件的说法是不正确的。事件时元素本身就具有的特征,只是触发事件后,默认没有相关的一些处理。这种操作其实就是给元素的某个事件添加一个事件处理函数。当事件被触发后,判断到属于该事件类型,就触发该事件函数的处理函数。
可以通过console.dir()把对象的所有属性和方法打印出来,查看对象或元素本身具有的事件。
结果:
获取元素,最好从父级元素获取,全部从document中获取,可能会出现混乱。
function func(形参1,形参2){
//函数执行代码
}
func(实参1,实参2);//调用时传参
什么时候使用到传参?当有两段代码本身的功能极其相似,只有个别地方不一样时,就可以把两段代码合并成一个函数,然后把两段代码中不一致的内容通过传参传进去。
案例:自定义(多个模块)选项卡封装
选项卡封装
内容一
内容二
内容三
内容一
内容二
内容三
内容一
内容二
内容三
index.js:
function _id(idName){
return document.getElementById(idName);
}
function _selector(parent,selector){
return parent.querySelector(selector);
}
function _selectorAll(parent,selectors){
return parent.querySelectorAll(selectors);
}
结果:
value和innerHTML都可以用来获取和修改元素的值(或内容);value只能获取特定的textarea和input的值,但是innerHTML可以获取所有HMTL元素的值。
不同之处如下:
1)value可以用来修改(获取)textarea和input的value属性的值或元素的内容;
2)innerHTML用来修改(获取)HTML元素(如div)html格式的内容。
修改input的值
结果:
案例:购物车商品累计。事先不知道用户买多少商品
不定参(可变参)使用关键字:arguments,代表所有实参的集合。通过下标获取参数的每一位;通过length获取实参的个数;
集合是类数组,可以使用下标,但是没有数组中的各种方法。
可变参(不定参):arguments
结果:
函数返回值即函数执行之后的返回结果。
结果:发现return后的代码没有继续执行
封装通过id/CSS选择器获取(一般在父级下获取,所以传入父级和选择器名字)获取多个元素的方法,然后返回获取到的值
//通过id名获取元素
function _id(idName){
return document.getElementById(idName);
}
//通过CSS选择器获取一个元素
function _selector(parent,selector){
return parent.querySelector(selector);
}
//通过CSS选择器获取一组元素
function _selectorAll(parent,selectors){
return parent.querySelectorAll(selectors);
}
使用:
var wrap1 = _id("wrap1");
var wrap2 = _id("wrap2");
var wrap3 = _id("wrap3");
var wraps = _selectorAll(document,".wrap");
var btn = _selectorAll(wraps,".tab button");
var divs = _selectorAll(wraps,".cont div");
点击时获取box的宽度,在原有基础上+100
标准浏览器下:transition:.5s;设置过渡效果的时间,否则div会直接变到从100+100的宽度
getComputedStyle(el)
结果:
不断点击后:
兼容问题:发现在IE6,7,8下并不支持getComputedStyle(el)方法
解决:通过el.currentStyle判断返回true代表在IE浏览器下,为false就不是在IE浏览器下。在IE浏览器下必须使用el.currentStyle才行
//解决浏览器兼容问题:通过el.currentStyle判断返回true代表在IE浏览器下,为false就不是在IE浏览器下。在IE浏览器下必须使用el.currentStyle才行
function currStyle(el,styleName){
// var curStyle = '';
// if(el.currentStyle){
// curStyle = el.currentStyle[styleName];
// }else{
// curStyle = getComputedStyle(el)[styleName];
// }
// return curStyle;
//使用三元获取返回值
return el.currentStyle?el.currentStyle[styleName]:getComputedStyle(el)[styleName];
}
getComputedStyle(el)
结果:
JS预解析机制(变量提升(Hoisting)):JS在读取到一个script标签(或者一个函数作用域)时,会先进行一个预解析的过程,在这个过程中,会把var声明的变量和function声明的函数体,提升到整个scriptt标签(或者一个函数作用域)最前边去。在预解析完之后,JS才会从上到下一行一行解析代码并执行。
//JS var变量的预解析
console.log("var变量的预解析:"+a);//undefined
var a = 0;
//JS函数体的预解析
console.log("函数体的预解析:"+fn);
function fn(){
console.log("函数");
}
//JS函数表达式的预解析
console.log("函数表达式的预解析:"+fnn);
var fnn = function(){
console.log("函数表达式");
};
结果:
在预解析时,会先预解析var,把var放在最前面,然后再预解析function,所以当var和function重名时,function会覆盖var:
/*
解析过程:先预解析var声明的a,再预解析函数体a,后面覆盖前面,所以最后结果是function函数体
var a;
function a(){console.log("函数a");};//此函数体解析后会覆盖变量a
*/
console.log(a);
var a = 0;
function a(){
console.log("函数a");
}
结果:
JS预解析示例:
//JS预解析过程:遇到Script标签或一个函数作用域,会先进行预解析,先预解析var声明的变量(包括普通变量声明和函数表达式声明),再声明function函数体,如果有重名function声明会覆盖var声明
/*
预解析之后代码:
var a;//变量a
var a;//函数表达式a(函数表达式也是var声明)
function a(){//函数体
console.log(1);
};
console.log(a);//打印函数体a
var a = 10;
console.log(a);//10
console.log(a);//10
a = function(){
console.log(2);//打印函数表达式a
};
console.log(a);
*/
console.log(a);
var a = 10;
console.log(a);
function a(){
console.log(1);
};
console.log(a);
var a = function(){
console.log(2);
};
console.log(a);
结果:
JS预解析机制不是良好的编码习惯,不利于代码维护,建议不要使用,编码时建议先声明,再使用。
扩展:从概念的字面意义上说,“变量提升”意味着变量和函数的声明会在物理层面移动到代码的最前面,但这么说并不准确。实际上变量和函数声明在代码里的位置是不会动的,而是在编译阶段被放入内存中。
ES6之后就不能像JS预解析这么编写JS代码了。
通常来说一段程序代码中使用的变量和函数并不总是可用的,限定其可用性的范围即作用域,作用域的使用提高了程序逻辑的局部性,增强程序的可靠性,减少名字冲突。
通俗的说,作用域:数据起作用的范围(某条数据可以在什么范围内使用)
前端权威官方MDN:https://developer.mozilla.org/en-US/docs/Glossary/Scope
作用域的分类:
全局作用域:
//声明在全局中的变量
var a = 0;
console.log(a);//可在全局任意地方调用
function fn(){
console.log(a);//可在函数中调用
a = 10;//可在任意地方修改全局中的变量
}
fn();
console.log(a);
结果:
//var声明的全局变量和function声明的全局函数,都默认挂载在window上
var a = 0;
console.log(window);
function fn(){
b = 10;
console.log(1);
}
fn();
console.log(b);//此处b没有写声明的var所以,b=10即为window.b = 10;相当于挂载在window上的全局变量
//在JS中,默认全局数据都会保存在window下
//window是浏览器的最顶层对象,所以默认window下的属性和方法都是全局的。所以window下的方法和属性,默认可以不写window
结果:全局变量a和全局函数fn()都默认挂载在window上。而且,此处b没有写声明的var,所以,b=10即相当于window.b = 10;也是挂载在window上的全局变量:
全局变量污染:大家都在全局中写代码,很容易造成命名冲突,导致代码冲突。ES6中代码冲突会直接报错。所以要养成好的习惯不要在全局去声明变量。
全局污染
结果:发现最后获取的只有一个元素,所以很容易造成代码冲突
解决:不要声明全局变量
(function(){
var list = document.getElementById("list");
console.log(list);
})();
(function(){
var list = document.querySelector(".memulist");
console.log(list);
})();
结果:
JS中提供了id使用的简便方法,直接调用id名即可:但尽量不要这么写,不规范:
console.log(list);
匿名函数:匿名函数自执行本身就是为了避免在全局写代码,避免冲突的。匿名函数自执行也叫开启一个新的命名空间。即开启新的作用域,此作用域和其他的不会冲突。
作用域链决定了哪些数据能被函数访问。当一个函数创建后,它的作用域链会被创建此函数的作用域中可访问的数据对象填充。
作用域链:JS中数据的查找规则。
作用域链查找过程:在JS中我们调用一条数据时,会先在当前作用域进行查找,如果找不到,就从向上找父作用域的数据,还找不到就接着向上,一直找到全局作用域(window对象),window都找不到就报错。
//调用fn()时,在其子函数fn2()被调用时,首先会在fn2()自己的作用域内找变量a
//找不到就在其父级作用域即fn()作用域中找,即a=10,然后打印a=10
function fn(){
var a = 10;
function fn2(){
console.log(a);
}
fn2();
}
fn();
结果:
作用域链查找关系图:
作用域链示例:
结果:
解析(函数拆分)三个f():
注意:这里的var f = fn();是将函数fn()的返回值函数体f(){ b++; console.log(b); };赋给变量f,但是并没有执行该返回值函数体,当f()调用时,便执行了该函数体。f此时是fn的子函数,那它可以访问和更改父级fn的作用域中的b。
b变量会一直赋值,是因为JS的垃圾回收机制决定的,只要检测都有引用存在,就不会释放。
作用域链示例-函数拆分
结果:所以执行三个f();和执行三个fn();得到的结果是不一样的。
以上进一步分解:
结果:所以调用三次f()和再fn()函数里执行三次fnn()是一样的
函数拆分fn()():
作用域链示例——函数拆分fn()()
结果:
函数每次调用,如fn()和fnA()之间没有任何关联,都相当于把这个代码复制出来执行了一遍。
闭包是对作用域链的一种表现和使用。
函数对象可以通过作用域链相互关联起来,函数体内的数据(变量和函数声明)都可以保存在函数作用域内,这种特性在计算机科学文献中被称为“闭包”。既函数体内的数据被隐藏于作用于链内,看起来像是函数将数据“包裹”了起来。从技术角度来说,js的函数都是闭包:函数都是对象,都关联到作用域链,函数内数据都被保存在函数作用域内。
fn()();调用函数后的返回值还是一个函数,也对其进行执行
函数的每次执行之间没有任何关系。每次执行都相当于在JS内部吧代码重新写了一遍。
面试时:闭包就是能够读取其他函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
闭包:
闭包示例:
闭包
结果:
闭包应用:i传参给了fn函数,而点击事件是fn函数的子函数,所以也可以获取到fn函数中的i
闭包应用
结果:按钮进行循环后会将当前index传参给fn函数,当点击按钮时,再通过父级作用域获取到父级的index
闭包应用二:(匿名函数自执行方式)页面刷新时解析for循环并将index传给fn,并且立即执行fn函数,当点击按钮时,再通过父级作用域fn获取到index
结果:
闭包应用三:点击按钮后,再立即执行一个匿名函数自执行。匿名函数自执行后,得到的是一个函数返回值,当点击时再执行该函数中的内容
结果:
默认情况下:
结果:
在script标签最上面加上 'use strict';,加上'use strict'后预解析已经不能使用,会报错。
严格模式下的function指向问题:在严格模式下,function如果不是作为对象的属性和方法被调用(即直接调用方法)就指向undefined。
结果:
结果:
结果:
总结:调用函数的bind方法,会生成新的函数,绑定的this指向是针对新函数的,新函数this指向被绑定后,不能再继续被绑定(call和apply也不行);如果调用时再传入新的参数,会将新的参数和被绑定的参数进行合并,被绑定的参数会一直存在;而原函数的this指向一直没有变,还可以继续调用bind方法,生成新的函数,同时给新的函数绑定新的this指向
结果:
为什么调用bind生成的新函数,this指向被绑定后就不能再绑定了?bind方法在ES5.1后才出来,如果想实现此功能,可以自己写可以再次绑定的bind方法。
自己实现bind方法:
参数:fn 要绑定this函数;_this返回新函数this指向谁
结果: