作用域是可访问变量的有效范围。
作用于所有代码执行的环境 (整个script标签内部) 或独立的js文件。
作用于函数内的代码环境,就是局部作用域。
在全局作用域下(函数外部)声明的变量叫做全局变量。
在局部作用域下(函数内部)声明的变量叫做局部变量。
// 全局变量/函数,可以覆盖window对象的变量/函数。 window.variable = 'window对象的变量'; var variable = '全局变量'; console.log(variable); // 返回:全局变量
// 局部变量/函数,可以覆盖window对象的变量和全局变量/window对象的函数和全局变量的函数。 function test4() { var variable = '局部变量'; console.log(variable); // 返回:局部变量 } test4();
出于种种原因,我们有时候需要得到函数内的局部变量。
但是,前面已经说过了,正常情况下,这是办不到的,
只有通过变通方法才能实现:那就是在函数的内部,再定义一个函数。
function fn1() { var num = 10; function fn2() { console.log(num); // 10 } }
在上面的代码中,函数fn2就被包括在函数fn1内部。这时fn1内部的所有局部变量,对fn2都是可见的。但是反过来就不行,fn2内部的局部变量,对fn1就是不可见的。
这就是Javascript语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
既然fn2可以读取fn1中的局部变量,那么只要把fn2作为返回值,我们不就可以在fn1外部读取它的内部变量了嘛。
function fn1() { // fn1函数就是闭包 var num = 10; function fn2() { return num; } return fn2; // fn2没有调用不执行,fn1的返回值是fn2函数 } var f = fn1(); // 调用fn1,得到返回值--fn2函数,把它保存到变量f中 f(); // 此时f保存的是fn2函数,调用该函数,就能得到fn2的返回值--num console.log(f()); // 10
闭包
就是能够读取其他函数内部变量的函数。(变量所在的函数就是闭包函数)
例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成——“定义在一个函数内部的函数“。
在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
注意:
闭包不是函数套函数
是因为需要局部变量,所以才把 num 放在一个函数里,如果不把 num 放在一个函数里,num 就是一个全局变量了,达不到使用闭包的目的——隐藏变量
闭包中的函数是可以没有return语句
因为如果不 return,你就无法使用这个闭包。把 return fn2 改成 window.fn2 = fn2 也是一样的,只要让外面可以访问到这个 fn2 函数就行了。
所以 return fn2 只是为了 fn2 能被使用,也跟闭包无关。
闭包可以用在许多地方。它的最大用处有两个:
<ul class="nav"> <li>项目1</li> <li>项目2</li> <li>项目3</li> <li>项目4</li> </ul>
var lis = document.querySelector('.nav').querySelectorAll('li'); for (var i = 0; i < lis.length; i++) { // 2. 所以给每个li动态的添加属性 lis[i].index = i; lis[i].onclick = function () { // 1. 因为点击事件函数是异步任务,点击了才会执行,但是for循环是同步任务,会立刻执行,所以会先循环完一遍,等到点击前i就已经是3了,点击后i变成4,所以不管点哪个都会输出4 // console.log(i); console.log(this.index); // this指向的是调用函数的对象,也就是点击的那一个 } }
// 立即执行函数也称为小闭包 因为立即执行函数里面的任何一个函数都可以使用它的i这个变量 for (var i = 0; i < lis.length; i++) { // 1. 循环的i是几 // 每次循环都会创建一个立即执行函数 (function (i) { // 3. 形参i接收传来的实参i // 点击事件函数访问了立即执行函数的参数i,立即执行函数就是个闭包函数 lis[i].onclick = function () { // 4. 哪一个被点击了 console.log(i); // 5. 就输出哪一个的索引号 } })(i) // 2. 就把i作为实参传给立即执行函数形参i }
var lis = document.querySelector('.nav').querySelectorAll('li'); for (var i = 0; i < lis.length; i++) { (function (i) { setTimeout(function () { console.log(lis[i].innerHTML); // 定时器函数使用了立即执行函数的参数i,立即执行函数就是个闭包函数 }, 3000) })(i); } // 若不把定时器函数放在立即执行函数内部,则会报错 原因同上,定时器也是异步任务,循环完i已经是3了,到定时器触发时i变为4,索引号4拿不到内容,所以报错
打车起步价13(3公里内), 之后每多一公里增加5块钱,用户输入公里数就可以计算打车价格
如果有拥堵情况,总价格多收取10块钱拥堵费
// 函数要声明及调用,这里方便起见,写成一个立即执行函数 price和yd函数使用了立即执行函数里的局部变量,所以立即执行函数就是个闭包函数 var car = (function () { var start = 13; // 起步价 局部变量 var total = 0; // 总价 局部变量 return { // 要返回2个函数,则把这2个函数放在一个对象里返回 // 正常的总价 price: function (n) { if (n <= 3) { total = start; } else { total = start + (n - 3) * 5 } return total; }, // 拥堵之后的费用 yd: function (flag) { return flag ? total + 10 : total; } } })(); console.log(car.price(5)); // 23 console.log(car.yd(true)); // 33 console.log(car.price(1)); // 13 console.log(car.yd(false)); // 13
原型和原型链是JavaScript进阶重要的概念,尤其在插件开发过程中是不能绕过的知识点。
了解原型我们从以下这个例子开始:
内置对象Array做一个数字排序
var arr1 = [1, 0, 0, 8, 6]; var arr2 = [1, 0, 0, 8, 6, 1, 1]; // arr1升序排序 arr1.sort(function (a, b) { return a - b; }); // arr2升序排序 arr2.sort(function (a, b) { return a - b; }); console.log(arr1); // [0, 0, 1, 6, 8] console.log(arr2); // [0, 0, 1, 1, 1, 6, 8] console.log(arr1 === arr2); // false 因为arr1和arr2中的元素和元素个数不同。 console.log(arr1.sort === arr2.sort); //true 因为arr1和arr2使用的是相同的sort方法,sort方法是arr1和arr2的公共方法
这个公共的sort方法不是arr1和arr2的方法,而是Array的方法;
我们通过Array创建了arr1和arr2这两个数组对象,此时arr1和arr2者两个数组对象会从Array继承到sort方法。
下面我们尝试给arr1增加一个getSum的方法:
arr1.getSum = function () { var sum = 0; for (var i = 0; i < this.length; i++) { sum += this[i]; } return sum; } // arr1可以正常调用getSum这个方法 var a = arr1.getSum(); console.log(a); // 15 // 但是arr2不能调用 因为arr2里没有getSum()方法 var b = arr2.getSum(); console.log(b); // 报错:arr2.getSum is not a function
由此可知 getSum() 不是公共方法,意味着 getSum() 在Array中没有。
如果希望arr2也能有getSum()求和方法,目前我们有2种做法:
那么,如何将getSum()求和方法交给Array呢?
答案:将实例属性/方法定义为原型属性/方法——Array.prototype.属性/方法
Array.prototype.getSum = function () { var sum = 0; for (var i = 0; i <this.length; i++) { sum += this[i]; } return sum; } console.log(arr1.getSum()); // 15 console.log(arr2.getSum()); // 17
要想知道 prototype 是什么?就需要从对象的创建开始了解:
var Person = { name: 'zhangsan', age: 20, address: '西安', test1: function () { console.log('Person对象中的方法'); } } console.log(Person.name); // zhangsan Person.test1(); // Person对象中的方法
我们可以把对象中一些公共的属性和方法抽取出来,封装到一个函数里面
// 先声明构造函数 function Man(name,age,address){ this.name=name; this.age=age; this.address=address; this.test = function(){ console.log('我是Man对象中的方法'); } } // 再使用关键字new实例化对象 var zs= new Man('张三',23,'西安'); var ls= new Man('李四',25,'北京'); // 此时实例化对象都可以使用构造函数Man里面的属性和方法 console.log(zs.address); // 西安 zs.test(); // 我是Man对象中的方法 console.log(ls.age); // 25 ls.test(); // 我是Man对象中的方法
问题:那么为什么构造函数创建出来的对象可以使用它的属性和方法呢?
答案:对象实例和它的构造函数之间建立一个链接(它是__proto__属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造函数中找到这属性和方法。
当我们通过构造函数创建对象的时候,实际上是创建了2个对象:
prototype
每个构造函数上面都有一个属性(prototype),指向了函数的原型对象(Man.prototype)。
即使你只定义了一个空函数,也存在一个prototype的属性。
console.log(Man.prototype); // {constructor: ƒ}
__proto__
)每个实例对象上面都有一个隐式原型(proto),指向了函数的原型对象
console.log(zs.__proto__); // {constructor: ƒ}
console.log(zs.__proto__ === Man.prototype); // true
实例访问属性或者方法的时候,遵循以下原则:
- 如果实例上面存在,就用实例本身的属性和方法。
- 如果实例上面不存在,就会顺着__proto__的指向一直往上查找,查找就停止。
// 下面给实例对象zs自身增加test方法: zs.test = function(){ console.log('我是zs实例对象的方法'); } // zs和构造函数的原型对象(Man.prototype)上面都有test方法,则zs实例调用自身的test方法 zs.test(); // 我是zs实例对象的方法 // ls自身没有test方法,会顺着ls对象的__proto__属性指向的原型找,看有没有test方法,结果找到了,则ls调用原型上面的test方法。 ls.test(); // 我是Man对象中的方法
对象原型( __proto__
)和构造函数(prototype)原型对象里面都有一个属性 :constructor 属性。
constructor 我们称为构造函数,因为它指回构造函数本身。
constructor 主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。
console.log(Man.prototype.constructor); // 输出Man构造函数 console.log(zs.__proto__.constructor); // 输出Man构造函数
__proto__
原型,指向构造函数的原型对象__proto__
属性,指向它的构造函数的原型对象Man原型对象prototype也是个对象,里面也有__proto__
原型,指向的是Object.prototype原型对象,Object.prototype原型对象是由Object构造函数创造出来的;
Object.prototype原型对象里面的__proto__
原型,指向为null
console.log(Man.prototype); // 里面也有一个__proto__,指向Object原型对象 console.log(Man.prototype.__proto__ === Object.prototype); // true console.log(Object.prototype.__proto__); // null