Skip to main content

JavaScript深度学习

· 24 min read
LIU

变量

  • 原始值 -> 基本类型

    • Number
    • String
    • Boolean
    • undefined
    • null
  • 引用值

    • 引用值就是拿的以下类型在堆内存的地址

    • object

    • array

    • function

    • date

    • RegExp

  • 栈内存 Stack

    • 原始值在占栈内存中存储
  • 堆内存 Heap

    • 引用值是存在堆内存当中,然后在栈内存中存储对应的指针地址
    • 修改原来的值会有影响,重新赋值没影响

image-7234340772

基本语法,运算符

  • 任何数据类型的值加上字符串,都得字符串

  • 字符串相对应的ASCII码(字符串相对应的十进制码),多个字符的,从左到右依次进行对比,直到比较出ASCII码大大小为止

//字符串比较
let bool = "1.5" > "11"; //true
  • NaN与任何东西都不相等,包括它自己

  • 有值范围的判断,或者条件多个,用if。 有定值的,用switch

  • 逻辑运算

undefined,null,NaN,"",0,-0,false

除上面以外全部都是真

循环

for(var i = 0 ; i<10; i++){
console.log(i)
}
//等同于
//for循环语句中,中间的语句是判断的作用,为真则执行
var i = 0;
for(;i<10;){
console.log(i);
i++;
}
//可转换成while
while(i<10){
console.log(i);
i++;
}

//终止循环,break等价于i=0
var i = 1;
for(;i;){
console.log(i);
i++;
if(i==11){
break; // i = 0;
}
}

//continue 跳过当前循环
//typeof 引用类型返回都是object
//number string boolean object undefined function
typeof(null) // object
typeof(undefined) //undefined
typeof(function(){}) //function

typeof(a) //undefined 未定义的变量,用typeof不会报错
//typeof的返回值是字符串类型
typeof(typeof(a)) //string

Number(true) // 1
Number('true') //NaN
Number(null) //0
Number(undefined) //NaN
Number('1a') //NaN
//会先通过Number(值)转换后,再判断
isNaN(undefined) //true
isNaN(null) //false

parseInt(true) //NaN
parseInt(null) //NaN
parseInt('ab12') //NaN
parseInt('12ab34 ') //12
parseInt('3.99') //3
parseInt('c',16) //12

parseFloat('3.147').toFixed(2) //3.15 (四舍五入)

'1'>2 //false
'a'<'b' // 转换成ascii再对比

NaN == NaN //false
null == 0 // false ,既不大于也不小于,也不等于
undefined == 0 // false ,既不大于也不小于,也不等于
undefined == null // true

斐波那契数列

function fibonacci (n) {
if ( n <= 1 ) {return 1};
return fibonacci(n - 1) + fibonacci(n - 2);
}

阶乘

function jc(num){
if(num<0) return 0;
if(num==1) return 1
return num * jc(num-1)
}

函数

// 函数声明
function test(){}

// 函数表达式,函数表达式
var test = function (){}

// 形参和实参长度
function test(a,b){
console.log(test.length) // 2
console.log(arguments.length) // 3
}
test(1,2,3)

// 可在函数内改变实参的值
function test(a,b){
a = 3
console.log(arguments[0]) // 3
}
test(1,2)

// 如果实参没有传值,在函数体内赋值是无效的
function test(a,b){
b = 3 //没有传实参
console.log(arguments[0]) // undefined,参数默认值是undefined
}
test(1)

function test(){
return //js引擎会隐式添加return
}

递归

p5 前半段

预编译

Javascript 引擎:

  1. 检查通篇的语法是否有错误

  2. 预编译的过程

  3. 解释一行,执行一行

test()  //正常执行调用
function test(){
console.log(1);
}

console.log(a) // undefined;
var a = 1

函数声明,整体提升。

变量只有声明提升,赋值是不提升的 (声明提升,字面量不提升)。

var a = 1;
b = 2;
// 同等,没有区别
console.log(window.b);// a = window.a
// b = window.b
window = { // 全局域
a:1,
b:2
}

function test(){
var a = b = 1; //函数内部没有用var声明变量而直接赋值,会提升到全局变量window上
}
test();
console.log(b)// 1
console.log(a)// a is not define
console.log(window.a)// undefined 访问对象里不存在的属性,默认是undefined
function test(b){
this.d = 3;
var a = 1;
function c(){}
}

test(123);
console.log(window.d) //3
const AO = {
b:undefined,
a:undefined,
c:function c(){},
arguments : [123],
this:window
}

函数上下文:

AO :activation object 活跃对象,函数上下文

  1. 提取形参和变量声明提升(形参和变量相同时,也是undefined)
  2. 实参值赋值给形参
  3. 函数声明提升 (形参和变量 与 函数名相同时,覆盖)
  4. 执行 (变量赋值)

形参和实参赋值,var变量声明提升,函数声明提升,执行

没var的变量不会提升,会执行到的时候才会直接赋值挂到window,如果在执行到当前代码之前使用,会报 x is not defined

函数声明提升赋值,执行的时候就不会再赋值

var a = 1;
function a(){
console.log(2)
}
console.log(a) //1
//----
function test(){
console.log(b)
if(a){
var b= 2; s
}
console.log(c) // 情况1:没用var声明的变量,不会提升,此处会报not defined
c = 3
console.log(c); // 情况2:执行以后 c 才会赋值到window下,3
}
var a;
test();
a = 1;
console.log(a);

作用域,作用域链

  1. [[scope]] 函数创建时,生成的一个js内部的隐式属性。

  2. [[scope]] -> scope chain (作用域链) -> GO -> AO

  3. 函数存储作用域链的容器,作用域链

    • AO / GO
      • AO,函数的执行期上下文
      • GO,全局的执行期上下文
      • 函数执行完成以后,AO是要销毁的,它是一个即时的存储容器
  4. 每一个函数在被定义时就包含全局的执行上下文 GO

  5. 在函数被执行前一刻生成AO (预编译)

a函数执行前一刻,生成[[scope]]

image-5211115235

  1. 所有函数自身的AO都是排在最顶端的

b函数执行前一刻,生成[[scope]]

image-5215431316

  1. 为什么外面无法访问函数内部的变量?因为作用域链没有内部函数的AO
function a(){
function b(){
function c(){
}
c()
}
b()
}
a()
image-5230133957

闭包

当内部函数被返回到外部并保存时,一定会产生闭包,闭包会产生 原来的作用域链 不释放。

过渡的闭包可能会导致内存泄露,或加载过慢。

image-5231729571

立即执行函数

instantly invoked function expression

//W3C规范
(function(){

}());
//或者
(function(){
})()

括号包起来的都叫表达式

一定是表达式才能被执行符号执行

// 可以
(function test1(){
console.log(1);
})()

// 函数表达式,可以
var test2 = function(){
console.log(1);
}();

// 语法错误
function test3(){
console.log(1);
}();

// 不报错
function test3(){
console.log(1);
}(4); //认为表达式

console.log((3,4)) //4

// 表达式忽略函数名
var a = 10;
if(function b(){}){ // (function b(){}) 用括号括起来为表达式,表达式忽略函数名,此事b为未定义,用typeof包裹不会报错,返回undefined
a+=typeof(b)
}
console.log(a) //10undefined
//累加器
function calc(){
var num = 0
function add(){
return num++
}
return add
}
//班级学生名字操作
function classroom(){
var students = []
function enter(name){
if(!students.includes(name)) {
students.push(name)
}
return students
}
function pop(name){
var idx = students.indexOf(name)
if(idx !==-1){
students.splice(i,1)
}
return students
}
function get(){
return students
}
return [enter,pop,get]
}

构造函数

  • 大驼峰
  • this在没有实例化的时候指向window,new实例化后指向对象
  • New的时候,实际上系统把this原本指向window的,转向实例化的对象
// 构造函数new原理
function Car(color,brand){
//AO = {
// this:{
// color:color,
// brand:brand
// }
//}
this.color = color;
this.brand = brand;
// return this
}
var car1 = new Car('red','Benz');

// 等同于
function Car(color,brand){
var me = {};
me.color = color;
me.brand = brand;
return me;
}
var car = Car('red','Benz')

// 如果return原始类型,实际上还是会隐式的return this
// 如果return引用类型,则会返回
function Car(){
this.color = 'red'
return 12 //无效,还是会返回this
return {} //返回空对象
}
var car = new Car();
console.log(car.color) //red

包装类

new Number(1)

new String('abc')

原始类型,访问属性,默认先new包装类,无法保存,执行delete

undefined 和 null 不能经过包装类转换为对象,所以没有toString()方法

var a = 123
a.len = 3 // undefined
//内部执行:
//new Number(123).len = 3 ; 无法保存,执行 delete

var a = new Number(123);
a.len = 3;
console.log(a.len) //3

//字符串本身没有length属性,是中间经过了一层包装类
var str = 'abc'
str.length = 1 //无效,不会改变。new String(str).length = 3 ; 无法保存,执行 delete
console.log(new String(str).length);

var arr = [1,2,3,4,5]
arr.length = 3
arr // [1,2,3] 会截断

原型

  • prototype是定义构造函数构造出的每个对象的公共祖先
 function Handphone (color,brand){
this.color = color;
this.brand = brand;
this.screen = '18:9';
this.system = 'Android'
}

Handphone.prototype.rom = '64G';
Handphone.prototype.ram = '6G';
Handphone.prototype.screen = '16:9';

var hp1 = new Handphone('red','小米');
var hp2 = new Handphone('black','华为');
console.log(hp1.rom); //64G 可以读取到原型上的属性
console.log(hp2.ram); //6G
console.log(hp1.screen); //18:9 构造函数自己里面有的,就不往原型上去找
  • constructor
function Telephone(){}

function Handphone(color){
this.color = color;
}
//constructor -> 默认指向构造函数本身

//可以更改
Handphone.prototype = {
constructor:Telephone
}

console.log(Handphone.prototype)


理解:

P10 中间

image-20211211224801304

Q:获取字节长度

//中文是两个字节,英文是一个字节
function getBytes(str){
var bytes = str.length;
for(var i = 0; i<str.length;i++){
if(str.charCodeAt(i)>255){
bytes++;
}
}
return bytes;
}

window和return

插件写法:

;(function(){
function Test(){
}
window.Test = Test;
})();

var test = new Test();

原型链

原型链 最重要的属性是 __proto__

原型链:沿着__proto__ 向上去找原型上的属性,就形成链条式的继承关系

原型链的顶端是Object.prototype

function Obj(){}
var o1 = new Obj();

o1.__proto__ === Obj.prototype //true

Obj.prototype.__proto__ === Object.prototype //true

Object.creat(对象) //创建对象,Object.creat(null):空对象,没有原型

function Obj(){}
Obj.prototype.num = 1;
var obj1 = Object.create(Obj.prototype);
var obj2 = new Obj();
//以上两种效果一致;
console.log(obj1);
console.log(obj2);

简易计算器插件:

;(function()){
var Compute = function(){
Compute.prototype = {
plus:function(a,b){
return a + b;
},
minus:function(a,b){
return a - b;
}
mul:function(a,b){
return a * b;
}
div:function(a,b){
return a / b;
}

window.Compute = Compute;
}
}
})();

call/apply

改变this指向

var a = [1,2,3]
Object.prototype.toString.call(a) // "[object Array]"

全局this -> window 预编译函数this -> window apply/call改变this指向 构造函数的this指向实例化对象

圣杯模式

Buffer?

function inherit(Target,Origin){
function Buffer(){}
Buffer.prototype = Origin.prototype;
Target.prototype = new Buffer();
}

链式调用

return this

hasOwnProperty

instanceof

function Car(){}
var car = new Car();

function Person()

console.log(car instanceof Car);

var a = []
console.log(a instanceof Array)

callee

var sum = (function(n){
if(n<=1){
return 1;
}
return n + arguments.callee(n-1);//找到本身的这个函数
})(100)

caller

test1();
function test1(){
test2()
}
function test2(){
console.log(test2.caller); //function test1(){}
}

数组

var arr = [];   //数组字面量


waterfall?

(0,function)()

https://stackoverflow.com/questions/40967162/what-is-the-meaning-of-this-code-0-function-in-javascript

//当您想调用方法而不将对象作为this值传递时:
var obj = {
method: function() { return this; },
v:2
};
obj.method(); // {method: ƒ, v: 2} this指向obj本身

(0,obj.method()) // {method: ƒ, v: 2}

obj.method()===obj // true
(0,obj.method()===obj) // true
(0,obj.method)()===obj // false 此处先获取method函数,再执行,所以this指向windows

//使用eval:
(function() {
(0,eval)("var foo = 123"); // indirect call to eval, creates global variable
})();
console.log(foo); // 123
(function() {
eval("var bar = 123"); // direct call to eval, creates local variable
})();
console.log(bar); // is not define
//根据上下文,它可能是参数分隔符而不是逗号运算符:
console.log(
function(a, b) {
return function() { return a; };
}
(123, function (arg) { /* ... */ })(this)
); // 123

module.exports

Node里面的模块系统遵循的是CommonJS规范。

那问题又来了,什么是CommonJS规范呢? 由于js以前比较混乱,各写各的代码,没有一个模块的概念,而这个规范出来其实就是对模块的一个定义。

CommonJS定义的模块分为: 模块标识(module)、模块定义(exports) 、模块引用(require)

先解释 exportsmodule.exports 在一个node执行一个文件时,会给这个文件内生成一个 exportsmodule对象, 而module又有一个exports属性。他们之间的关系如下图,都指向一块{}内存区域。

exports = module.exports = {};

clipboard.png

module.exports = {
b:2
}

exports.a = 1
exports.c = 2
////
const d = require('./exports.js')
console.log(d) //{b:2} module.exports优先

// ----------

//module.exports = {
// b:2
//}

exports.a = 1
exports.c = 2

const d = require('./exports.js')
console.log(d) //{a:1,c:2}
//utils.js
let a = 100;

console.log(module.exports); //能打印出结果为:{}
console.log(exports); //能打印出结果为:{}

exports.a = 200; //这里辛苦劳作帮 module.exports 的内容给改成 {a : 200}

exports = '指向其他内存区'; //这里把exports的指向指走

//test.js

var a = require('/utils');
console.log(a) // 打印为 {a : 200}

从上面可以看出,其实require导出的内容是module.exports的指向的内存块内容,并不是exports的。 简而言之,区分他们之间的区别就是 exports 只是 module.exports的引用,辅助后者添加内容用的。

用白话讲就是,exports只辅助module.exports操作内存中的数据,辛辛苦苦各种操作数据完,累得要死,结果到最后真正被require出去的内容还是module.exports的,真是好苦逼啊。

其实大家用内存块的概念去理解,就会很清楚了。

然后呢,为了避免糊涂,尽量都用 module.exports 导出,然后用require导入。

export / export default

ES中的模块导出导入

在es中的模块,就非常清晰了。不过也有一些细节的东西需要搞清楚。 比如 exportexport default,还有 导入的时候,import a from ..,import {a} from ..,总之也有点乱,那么下面我们就开始把它们捋清楚吧。

它们的区别

  1. export与export default均可用于导出常量、函数、文件、模块等
  2. 在一个文件或模块中,export、import可以有多个,export default仅有一个
  3. 通过export方式导出,在导入时要加{ },export default则不需要
  4. export能直接导出变量表达式,export default不行。

下面咱们看看代码去验证一下

export function a (){
console.log('fun')
}

const b = {a:222}
export default b;
////
import a from './export'
console.log(a) //默认导入export default的
//----
import {a} from './export'
console.log(a) //fun ,导入export的

可选链操作符 ?

a.b.c.d

中间值如果取不到会报错

a?.b?.c?.d

而在当前的 ES2020 中,我们可以使用可选链操作符优雅获取,不会报错

typeScript

学习泛型:Type-challenges

mdn

方法的polyfill 实现、数组扩展方法的重写、函数静态方法

数据类型的判断

typeof 可以正确识别:undefined、boolean、number、string、symbol、function 等类型的数据,但是对于其它的都会认为是 object, 比如 Null、Date 等,所以通过 typeof 来判断数据类型不准确

function typeof(obj){
return Object.prototype.toString.call(obj).slice(8,-1).toLowerCase();
}

typeOf([]) // 'array'
typeOf({}) // 'object'
typeOf(new Date) // 'date'

数组去重

Array.filter(function(currentValue, indedx, arr), thisValue)

ES5:

function unique(arr){
let res = arr.filter(function (item,index,array){
return array.indexOf(item) === index
})
return res
}

ES6:

var unique = arr => [...new Set(arr)]

数组扁平化

数组扁平化就是将 [1, [2, [3]]] 这种多层的数组拍平成一层 [1, 2, 3]。使用 Array.prototype.flat 可以直接将多层数组拍平成一层:

[1, [2, [3]]].flat(2)  // [1, 2, 3]

实现 flat 这种效果。

ES5 实现:递归。

function flat2(arr){
let res = []
for(let i = 0,len=arr.length;i<len;i++){
if(Array.isArray(arr[i]){
res = res.concat(flat2(arr[i]))
}else{
res.push(arr[i])
}
}
return res;
}

ES6 实现:

function flat3(arr){
while(arr.some(item=>Array.isArray(item))){
arr = [].concat(...arr)
}
return arr;
}

深浅拷贝

浅拷贝:只考虑对象类型

function shallowCopy(obj) {
if (typeof obj !== 'object') return

let newObj = obj instanceof Array ? [] : {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key]
}
}
return newObj
}

简单版深拷贝:只考虑普通对象属性,不考虑内置对象和函数

function deepClone(obj) {
if (typeof obj !== 'object') return;
var newObj = obj instanceof Array ? [] : {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key];
}
}
return newObj;
}

javascript的执行上下文和执行栈

执行上下文是当前javascript代码被解析和执行所在环境的抽象概念。

执行上下文三种类型

  1. 全局执行上下文:默认的,最基础执行上下文

    • 不在函数内部的代码都位于全局执行上下文中

    • 创建一个全局对象,就是window对象

    • 将this指针指向这个全局对象

  2. 函数执行上下文:每次函数被调用时,都会创建一个新的执行上下文

    • 每个函数都有自己的执行上下文
    • 一个程序中可以存在任意数量的函数执行上下文
  3. eval函数执行上下文:运行在eval函数中的代码,也能获得自己的执行上下文,很少用不建议用

执行栈

执行栈,也叫调用栈,具有LIFO(后进先出)结构,用于存储在代码执行期间创建的所有执行上下文。

执行上下文的声明周期

  1. 创建阶段
  1. 执行阶段

  2. 回收阶段

Object.defineProperty

object.defineproperty ()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

备注:应当直接在obiect构造器对象上调用此方法,而不是在任意一个object类型的实 例上调用

let obj = {};
let newObj = Object.defineProperty(obj,'a',{
value:1
});

console.log(newObj === obj); // true

Object.defineProterty(obj,prop,descriptor);

obj 要定义属性的对象

prop 要定义或修改的属性的名称

descriptor 要定义或修改的属性描述符

描述

该方法允许精确地添加或修改对象的属性。通过赋值操作添加的普通属性是可枚举的,在枚举对象属性时会被枚举到(for...inObject.keys 方法),可以改变这些属性的值,也可以删除这些属性。这个方法允许修改默认的额外选项(或配置)。默认情况下,使用 Object.defineProperty() 添加的属性值是不可修改(immutable)的。

对象里目前存在的属性描述符有两种主要形式:数据描述符存取描述符数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。存取描述符是由 getter 函数和 setter 函数所描述的属性。一个描述符只能是这两者其中之一;不能同时是两者。

这两种描述符都是对象。它们共享以下可选键值(默认值是指在使用 Object.defineProperty() 定义属性时的默认值):

  • configurable

    当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。 默认为 false

  • enumerable

    当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。 默认为 false

数据描述符还具有以下可选键值:

  • value

    该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。 默认为 undefined

  • writable

    当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符 (en-US)改变。 默认为 false

存取描述符还具有以下可选键值:

  • get

    属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的 this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。 默认为 undefined

  • set

    属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。 默认为 undefined

持续学习,持续记录...