作用域理解:定义的变量、函数生效的范围。javascript 有全局作用域和函数作用域两种。注:es6实现let 块级作用域不是js原生的,底层同样是通过var实现的。
执行上下文
范围:一段内或者一个函数内;
全局:函数声明、变量声明 。范围:所有地方生效;
函数:函数声明、变量声明、this、arguments。范围:一个函数内部;
var site = "你好!";
console.log(site);
console.log(window.site);
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();
{
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得词法(静态)作用域,即作用域链在函数创建的时候就确定了。
子函数如果不被销毁,整条作用域链上的变量仍然保存在内存中。
// 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++;
}
const colors = ['red','green','blue','yellow']
let i = 0;
do {
console.log(colors[i]);
i++;
}while(i<colors.length);
let s = 0;
for (let i = 1;i<=100;i++){
s += i;
}
console.log(s);
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());
常见的迭代器如下:
最早的数组遍历方式
var a = ["a", "b", "c"];
for(var index = 0;index < a.length;index++){
console.log(a[index]);
}
for循环,我们最熟悉也是最常用的循环迭代方式,后来的许多迭代方法都是基于for循环封装的。
[].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);
})
var myArry =[1,2,3,4];
myArry.desc ='four';
for(let value of myArry){
console.log(value)
}
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
语法:
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);
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]
}));
函数有二个功能:
在js中没有“类”的概念,都是通过原型来实现继承的
构造函数与其他函数的唯一区别,就在于调用它们的方式不同。不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过 new 操作符来调用,那它就可以作为构造函数,如果不通过 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
根据上面代码,看下图:
需要理解三点:
更简单的原型语法
我们不可能总像之前的例子一样,没添加一个属性和方法就要敲一遍,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就可以获得到父类原型上的方法。
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 中最常用的继承模式。不过也存在缺点就是无论在什么情况下,都会调用两次构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数的内部,子类型最终会包含父类型对象的全部实例属性,但我们不得不在调用子类构造函数时重写这些属性。
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)
编程界崇尚以简洁优雅为美,很多时候
如果你不能向一个六岁的孩子解释清楚,那么其实你自己根本就没弄懂。
如果你觉得一个概念很复杂,那么很可能是你理解错了。