作用域
作用域理解:定义的变量、函数生效的范围。javascript 有全局作用域和函数作用域两种。注:es6实现let 块级作用域不是js原生的,底层同样是通过var实现的。
执行上下文
范围:一段内或者一个函数内;
全局:函数声明、变量声明 。范围:所有地方生效;
函数:函数声明、变量声明、this、arguments。范围:一个函数内部;
- 全局作用域
全局对象:如果是在浏览器中运行js,那么全局对象就是window。
var site = "你好!";
console.log(site);
console.log(window.site);
- 函数作用域
JavaScript 拥有函数作用域:每个函数创建一个新的作用域。
作用域决定了这些变量的可访问性(可见性)。
函数内部定义的变量从函数外部是不可访问的(不可见的)。
var site = "hello world";
function getSite(){
// 私有变量(局部变量)仅在当前作用域,外部不可见
var domain = 'zhsh66.club';
console.log(domain);
// 优先访问内部作用域中的同名变量,找不到才到上一层中访问,直到全局window
var site = 'zhang';
// site 是声明在函数外部的全局变量
// 但是在函数内部可以访问到全局变量
return `${site}[${domain}]`;
}
console.log(getSite()) // zhang[zhsh66]
console.log(domain); // domain is not defined
- 作用域链
自由变量:当前作用域没有定义的变量,但是上层作用域定义了,可以在此层作用域中使用的变量。
作用域链:当使用一个变量时候,先在自己的作用域里找,如果没有找到,再到父级作用域找,一直找到全局作用域,如果都没有找到即报错。
var name = 'zhang';
function fn1(){
var age = 18;
function fn2(){
var gender = 'man';
console.log(name,age,gender); //zhang 18
}
fn2();
}
fn1();
- 块级作用域
注意:js无块级作用域,仅有全局作用域和函数作用域两种作用域。
ES6 通过对全局作用域的特殊实现,实现了js的块级作用域 let;
let 的生效范围为 { };
const 为定义常量。注:定义的常量的值存储的内存地址不可变动,值是可变的比如常量中定义的是数组 或者对象时候,可以通过数组或对象方法操作原数据,只要不重新赋值就没问题
{
var s = 666;
let a = 1;
const B = "hello";
}
console.log(s); // 666 var 不支持块级作用域
console.log(a,B); // not defined
闭包
闭包就是能够读取其他函数内部变量的函数。由于在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数”。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
JavaScript闭包,在JavaScript中允许函数定义和函数表达式位于另一个函数的函数体中(内部函数),而且内部函数可以访问它们所在外部函数声明中的所有局部变量,参数以及其他内部函数。当其中一个内部函数被外部函数以外调用时就会形成闭包。
function testFn(){
var localVar = 10; // 自由变量
function innerFn(innerParam){
console.log(innerParam+localVar);
}
return innerFn;
}
var someFn = testFn();
someFn(20) // 30
一般来说,在函数执行完毕之后,局部变量对象即被销毁,所以innerFn是不可能以返回值形式返回的,innerFn函数作为局部变量应该被销毁才对。
利用自调用函数创建的闭包
var foo = {};
(function(obj){
var x= 10; // 自由变量
obj.getX = function(){
return x;
};
})(foo);
console.log(foo.getX()); // 获得闭包“x” 10
总结的闭包概念
闭包就是子函数可以有权访问父函数的变量、父函数的父函数的变量、一直到全局变量。归根结底,就是利用js得词法(静态)作用域,即作用域链在函数创建的时候就确定了。
子函数如果不被销毁,整条作用域链上的变量仍然保存在内存中。
循环控制
- while
- do while
- for
- for-in
- while:入口判断 (至少执行0次)
// 1.循环变量初始化
// 2.循环条件
// 3.更新循环变量
const colors = ['red','green','blue','yellow']
let i = 0;
while(i<colors.length){
console.log("%c%s","color:pink",colors[i]);
i++;
}
- do while :出口判断(至少执行1次)
const colors = ['red','green','blue','yellow']
let i = 0;
do {
console.log(colors[i]);
i++;
}while(i<colors.length);
- for 循环
let s = 0;
for (let i = 1;i<=100;i++){
s += i;
}
console.log(s);
- for-in 方法
for…in 适合进行普通对象的遍历 for-in循环遍历对象的key,即键值对的键名。 特性如下: 1. 方法遍历到的index(或key)
const lesson = {"my-id":1,name:"JavaScript编程",score:88};
console.log(lesson);
// key 键 name socre
for(let key in lesson){
// lesson.key 这里key是变量,所以需要使用lesson[key]方式来获取
// key.my-id非法标识符
console.log(lesson[key]);
}
迭代器
ES6中引入了 JavaScript 迭代器,它们用于循环一系列的值,通常是某种类型的集合。根据官方定义,迭代器必须实现一个next()函数,该函数以{value,done}的形式返回一个对象,其中 value 是迭代序列中的下一个值,done 是一个布尔值,用于确定序列是否已被使用。
下面手动模拟实现一个迭代器
function myIterator(data){
// 迭代器中必须要有一个next()方法
let i = 0;
return {
next() {
// done: 表示遍历是否完成
// value:当前正在遍历的数据
let done = i >= data.length;
let value = !done?data[i++]:undefined;
return {done,value}
}
}
}
let iterator = myIterator(['html','css','js','jquery']);
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
常见的迭代器如下:
- forEach迭代器
- for-of迭代器
- every迭代器
- some迭代器
- reduce迭代器
- map迭代器
- fiter迭代器
最早的数组遍历方式
var a = ["a", "b", "c"];
for(var index = 0;index < a.length;index++){
console.log(a[index]);
}
for循环,我们最熟悉也是最常用的循环迭代方式,后来的许多迭代方法都是基于for循环封装的。
- forEach方法
forEach遍历数组,而不是遍历对象哦,而且在遍历的过程中不能被终止,必须每一个值遍历一遍后才能停下来
语法:[].forEach(function(value, index, array) { // ... });
let arr = [1,2,3,4,5,6];
// value 值 index 索引 array 数组对象
arr.forEach((value,index,array)=>{
console.log(value,index,array);
})
- for-of 方法
for-of循环适合遍历数组
var myArry =[1,2,3,4];
myArry.desc ='four';
for(let value of myArry){
console.log(value)
}
- 这是最简洁、最直接的遍历数组元素的语法。
- 这个方法避开了for-in循环的所有缺陷,解决了forEach的不可中断问题。
- for…of为ES6新增方法。
- every迭代器
every方法接受一个返回值为布尔类型的函数,对数组中的每个元素使用这个函数,如果对于所有的元素,该函数均返回true,则该方法返回true,否则返回false - some迭代器
some方法也是接受一个返回值为布尔类型的函数,只要有一个元素使得该函数返回true,该方法就返回true
every()是对数组中每一项运行给定函数,如果该函数对每一项返回true,则返回true。
var arr = [1, 2, 3, 4, 5];
console.log(arr.some((item, index, array) => {
return item > 3
})); // true
some()是对数组中每一项运行给定函数,如果该函数对任一项返回true,则返回true。
var arr = [1, 2, 3, 4, 5];
console.log(arr.every((item, index, array) => {
return item > 3
})); // false
- reduce迭代器
reduce方法接受一个函数,返回一个值,该方法从一个累加值开始,不断对累加值和数组中的后续元素调用该函数,知道数组中最后一个元素,最后得到返回的累加值
语法:
arr.reduce(function(prev,cur,index,arr){
...
}, init);
arr 表示原数组;
prev 表示上一次调用回调时的返回值,或者初始值 init;
cur 表示当前正在处理的数组元素;
index 表示当前正在处理的数组元素的索引,若提供 init 值,则索引为0,否则索引为1;
init 表示初始值。
var arr = [3,9,4,3,6,0,9];
// 求数组项之和
var sum = arr.reduce((prev, cur)=>{
return prev + cur;
},0);
- map迭代器
map迭代器和forEach有些类似,但是map会改变数组,生成新的数组
var numbers = [25,36,121,49];
// arr 数组 index 索引 value值
console.log(numbers.map((value,index,arr)=>{
console.log('arr',arr);
console.log('index',index);
console.log('value',value);
return value*2; // [50, 72, 242, 98]
}));
- fiter迭代器
fiter迭代器和every迭代器类似,传入一个返回值为布尔类型的函数,和every方法不同的是,当数组中所有元素对应该函数返回的结果均为true时,该方法并不返回true,而是返回一个新的数组,该数组包含对应函数返回结果为true的元素
构造函数
函数有二个功能:
- 基本功能是封装操作步骤
- 扩展功能当成对象的构造器、构造函数、对象生成器来用
在js中没有“类”的概念,都是通过原型来实现继承的
构造函数与其他函数的唯一区别,就在于调用它们的方式不同。不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过 new 操作符来调用,那它就可以作为构造函数,如果不通过 new 操作符来调用,那它跟普通函数也不会有什么两样。
构造函数的三大特点:
- a:构造函数的函数名的第一个字母通常大写。
- b:函数体内使用this关键字,代表所要生成的对象实例。
- c:生成对象的时候,必须使用new命令来调用构造函数。
function Keith() {
this.height = 180;
this.weight = "120kg";
}
var boy = Keith(); // undefined
var boy = new Keith(); // Object { height: 180, weight: "120kg" }
console.log(boy.height); //180
使用 new 操作符来调用构造函数,会经历以下4个步骤:
① 创建新对象
② 将构造函数的作用域赋值给新对象(因此 this 就指向了这个新对象)
③ 执行构造函数中的代码(为这个新对象添加属性)
④ 返回新对象
使用new命令时,根据需要,构造函数也可以接受参数。
function Person(name, height) {
this.name = name;
this.height = height;
}
var boy = new Person('Keith', 180);
console.log(boy.name); //'Keith'
console.log(boy.height); //180
var girl = new Person('Samsara', 160);
console.log(girl.name); //'Samsara'
console.log(girl.height); //160
girl.__proto__===boy.__proto__;
boy.__proto__===Person.prototype
上面代码中,首先,我们创建了一个构造函数Person,传入了两个参数name和height。构造函数Person内部使用了this关键字来指向将要生成的对象实例。
然后,我们使用new命令来创建了两个对象实例boy和girl。
当我们使用new来调用构造函数时,new命令会创建一个空对象boy,作为将要返回的实例对象。接着,这个空对象的原型会指向构造函数Person的prototype属性。也就是boy.__proto__===Person.prototype
的。要注意的是空对象指向构造函数Person的prototype属性,而不是指向构造函数本身。然后,我们将这个空对象赋值给构造函数内部的this关键字。也就是说,让构造函数内部的this关键字指向一个对象实例。最后,开始执行构造函数内部代码。
因为对象实例boy和girl是没有name和height属性的,所以对象实例中的两个属性都是继承自构造函数Person中的。这也就说明了构造函数是生成对象的函数,是给对象提供模板的函数。
原型、原型链
我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以有由特定类型的所有实例共享的属性和方法。
function Person(){
this.show = function () {}
}
// 构造函数对象的原型对象上的成员,可以被所以示例所共享
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
console.log(this.name);
}
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
console.log(person1.sayName === person2.sayName); //true
console.log(person1.show === person2.show); // false
根据上面代码,看下图:
需要理解三点:
- 我们只要创建了一个新的函数,就会根据一组特定的规则为该函数创建一个prototype属性,指向函数的原型对象。即Person(构造函数)有一个prototype指针,指向Person.prototype
- 默认情况下,每个原型对象上都会创建一个constructor(构造函数)属性,这个属性是一个指向prototype属性所在函数的指针
- 每个实例的内部都有一个指针(内部属性) ,指向构造函数的原型对象。即 person1 和person2 身上都有一个内部属性proto(在ECMAscript中管这个指针叫[[prototype]],虽然在脚本中没有标准的方式访问[[prototype]],但是firefox,ie,chrome都支持一个属性叫proto) 指向Person.prototype
注意:person1 和person2 实例与构造函数之间没有直接的关系。
更简单的原型语法
我们不可能总像之前的例子一样,没添加一个属性和方法就要敲一遍,Person.prototype。为了减少不必要的输入,更常见的方法是像下面这样:
function Person(){}
Person.prototype = {
name: 'ccc',
age: 18,
sayName: function () {
console.log(this.name)
}
}
在上面代码中,我们将Person.prototype设置为等于一个以对象字面量形式创建的新对象。最终结果相同,但有一个例外,constructor属性不再指向Person了。前面我们介绍过,每创建一个函数,就会同时创建它的prototype对象,这个对象也会自动获得constructor属性。但是在我们使用的新语法中,本质上完全重写了默认的prototype对象,因此constructor属性也就变成了新对象的constructor属性(指向Object构造函数),不再指向Person函数了。此时,尽管instanceof操作符还能返回正确的结果,但通过constructor已经无法确定对象的类型了。如下:
var person1 = new Person()
console.log(person1 instanceof Object) // --> true
console.log(person1 instanceof Person) // --> true
console.log(person1.constructor === Person) // --> false
console.log(person1.constructor === Object) // --> true
这里用instanceof操作符测试Object和Person仍然返回true,constructor属性则等于Object,不等于Person了,如果constructor真的很重要可以像下面这样写:
function Person(){}
Person.prototype ={
constructor: Person, // --> 重设
name: 'ccc',
age: 18,
sayName: function () {
console.log(this.name)
}
}
所有的引用类型默认都继承了Object,而这个继承也是通过原型链实现的。所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype,这也正是所有自定义类型都会有toString(),valueOf()方法的原因。
构造函数、原型对象和实例的关系
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。
类的继承
类就是创建对象的模板
JavaScript 常见几种继承方式:
- 方式一、原型链继承
这种方式关键在于:子类型的原型为父类型的一个实例对象。
//父类型
function Person(name, age) {
this.name = name,
this.age = age,
this.play = [1, 2, 3]
this.setName = function () {}
}
// 构造函数对象的原型对象上的成员,可以被所有实例共享
Person.prototype.setAge = function () {}
//子类型
function Student(price) {
this.price = price
this.setScore = function () {}
}
Student.prototype = new Person() // 子类型的原型为父类型的一个实例对象
var s1 = new Student(15000)
var s2 = new Student(14000)
console.log(s1,s2)
这种方式实现的本质是通过将子类的原型指向了父类的实例,所以子类的实例就可以通过proto访问到 Student.prototype 也就是Person的实例,这样就可以访问到父类的私有方法,然后再通过proto指向父类的prototype就可以获得到父类原型上的方法。
- 方式二: 借用构造函数继承
这种方式关键在于:在子类型构造函数中通用call()调用父类型构造函数
function Person(name, age) {
this.name = name,
this.age = age,
this.setName = function () {}
}
Person.prototype.setAge = function () {}
function Student(name, age, price) {
Person.call(this, name, age) // 相当于: this.Person(name, age)
/*this.name = name
this.age = age*/
this.price = price
}
var s1 = new Student('Tom', 20, 15000)
这种方式只是实现部分的继承,如果父类的原型还有方法和属性,子类是拿不到这些方法和属性的。
- 方式三: 原型链+借用构造函数的组合继承
这种方式关键在于:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用。
function Person (name, age) {
this.name = name,
this.age = age,
this.setAge = function () { }
}
Person.prototype.setAge = function () {
console.log("111")
}
function Student (name, age, price) {
Person.call(this, name, age)
this.price = price
this.setScore = function () { }
}
Student.prototype = new Person()
Student.prototype.constructor = Student//组合继承也是需要修复构造函数指向的
Student.prototype.sayHello = function () { }
var s1 = new Student('Tom', 20, 15000)
var s2 = new Student('Jack', 22, 14000)
console.log(s1)
console.log(s1.constructor) //Student
console.log(s2.constructor.prototype) //Person
// ser对象的原型属性永远指向他的构造函数的原型属性对象
// Student的构造函数的原型 s1对象的原型
Student.prototype===s1.__proto__ // true
这种方式融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。不过也存在缺点就是无论在什么情况下,都会调用两次构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数的内部,子类型最终会包含父类型对象的全部实例属性,但我们不得不在调用子类构造函数时重写这些属性。
- 方式四、ES6中class 的继承
ES6中引入了class关键字,class可以通过extends关键字实现继承,还可以通过static关键字定义类的静态方法,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。
class Person {
//调用类的构造方法
constructor(name, age) {
this.name = name
this.age = age
}
//原型方法(共享),通过对象来调用的
showName () {
console.log("调用父类的方法")
console.log(this.name, this.age);
}
// 静态方法:不需要实例化(new class),直接用类名点来调用
static fetch(){
console.log("static function")
}
// 静态属性
static userName = "灭绝小师妹";
// 私有成员
#weight = 50;
// 访问器属性
get weight(){
return this.#weight
}
set weight(weight){
this.#weight = weight;
}
}
let p1 = new Person('kobe', 39)
console.log(p1)
//定义一个子类
class Student extends Person {
constructor(name, age, salary) {
super(name, age)//通过super调用父类的构造方法
this.salary = salary
}
// 优先访问自己的方法
showName () {//在子类自身定义方法
console.log("调用子类的方法")
console.log(this.name, this.age, this.salary);
}
}
let s1 = new Student('wade', 38, 1000000)
console.log(s1)
s1.showName()
作业:
- 实例演示作用域与闭包;
let n = 10;
function fn(){
let n = 20;
function f(){
n++;
console.log(n);
}
return f;
}
var x = fn();
x(); // 21
// --------------------
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0](); // 3
data[1](); // 3
data[2]() // 3
// 这里的 i 是全局下的 i,共用一个作用域,当函数被执行的时候这时的 i=3,导致输出的结构都是3。
// 可以使用具有块级作用域的let完美解决
- 实例演示类与类的继承
class Person{
// 构造函数
constructor(name,age){
this.name = name;
this.age = age;
}
// 原型方法
walk () {
console.log(`I am walking`)
}
// 静态方法
static eat () {
console.log(`I am eating`)
}
// 静态属性
static msg = "我是一个静态属性";
// 私有成员
#myPrivate = "这是一个私有成员";
// 通过访问器属性 读取和设置私有属性
get myPrivate(){
return this.#myPrivate;
}
set myPrivate(value){
this.#myPrivate = value;
}
}
class Student extends Person{
constructor(name,age,gender){
super(name,age);
this.gender = gender;
}
run () {
console.log('I can run')
}
}
let p1 = new Person('zhang',18);
console.log(p1);
let s1 = new Student('shuai',18,true);
console.log(s1)
编程界崇尚以简洁优雅为美,很多时候
如果你不能向一个六岁的孩子解释清楚,那么其实你自己根本就没弄懂。
如果你觉得一个概念很复杂,那么很可能是你理解错了。